From 885fe9be3a1e9486c78504a37f45366f929db4d4 Mon Sep 17 00:00:00 2001 From: Michael Zanetti Date: Mon, 8 Nov 2021 18:31:07 +0100 Subject: [PATCH 01/20] Add nymea energy experience plugin --- .gitignore | 2 + config.pri | 10 ++ debian/changelog | 3 + debian/compat | 1 + debian/control | 42 ++++++ debian/libnymea-energy-dev.install.in | 3 + debian/libnymea-energy.install.in | 3 + .../nymea-experience-plugin-energy.install.in | 1 + debian/rules | 27 ++++ libnymea-energy/energymanager.cpp | 37 +++++ libnymea-energy/energymanager.h | 65 +++++++++ libnymea-energy/energyplugin.cpp | 61 ++++++++ libnymea-energy/energyplugin.h | 66 +++++++++ libnymea-energy/libnymea-energy.pro | 38 +++++ nymea-experience-plugin-energy.pro | 7 + plugin/energyjsonhandler.cpp | 98 +++++++++++++ plugin/energyjsonhandler.h | 31 ++++ plugin/energymanagerimpl.cpp | 135 ++++++++++++++++++ plugin/energymanagerimpl.h | 45 ++++++ plugin/experiencepluginenergy.cpp | 119 +++++++++++++++ plugin/experiencepluginenergy.h | 64 +++++++++ plugin/plugin.pro | 42 ++++++ 22 files changed, 900 insertions(+) create mode 100644 .gitignore create mode 100644 config.pri create mode 100644 debian/changelog create mode 100644 debian/compat create mode 100644 debian/control create mode 100644 debian/libnymea-energy-dev.install.in create mode 100644 debian/libnymea-energy.install.in create mode 100644 debian/nymea-experience-plugin-energy.install.in create mode 100755 debian/rules create mode 100644 libnymea-energy/energymanager.cpp create mode 100644 libnymea-energy/energymanager.h create mode 100644 libnymea-energy/energyplugin.cpp create mode 100644 libnymea-energy/energyplugin.h create mode 100644 libnymea-energy/libnymea-energy.pro create mode 100644 nymea-experience-plugin-energy.pro create mode 100644 plugin/energyjsonhandler.cpp create mode 100644 plugin/energyjsonhandler.h create mode 100644 plugin/energymanagerimpl.cpp create mode 100644 plugin/energymanagerimpl.h create mode 100644 plugin/experiencepluginenergy.cpp create mode 100644 plugin/experiencepluginenergy.h create mode 100644 plugin/plugin.pro 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/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..83ce535 --- /dev/null +++ b/libnymea-energy/energymanager.h @@ -0,0 +1,65 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* 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 + +#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; + +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..fb31ade --- /dev/null +++ b/libnymea-energy/libnymea-energy.pro @@ -0,0 +1,38 @@ +TEMPLATE = lib +TARGET = $$qtLibraryTarget(nymea-energy) + +include(../config.pri) +NYMEA_ENERGY_VERSION_STRING = "0.0.1" + +CONFIG += link_pkgconfig +PKGCONFIG += nymea + + +HEADERS += \ + energymanager.h \ + energyplugin.h + +SOURCES += \ + 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..c6ee7d9 --- /dev/null +++ b/plugin/energyjsonhandler.cpp @@ -0,0 +1,98 @@ +#include "energyjsonhandler.h" +#include "energymanagerimpl.h" + +Q_DECLARE_LOGGING_CATEGORY(dcEnergyExperience) + +EnergyJsonHandler::EnergyJsonHandler(EnergyManager *energyManager, QObject *parent): + JsonHandler(parent), + m_energyManager(energyManager) +{ + registerEnum(); + + 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)); + registerMethod("GetPowerBalance", 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)); + registerNotification("PowerBalanceChanged", 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()); + emit PowerBalanceChanged(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()); + return createReply(ret); +} diff --git a/plugin/energyjsonhandler.h b/plugin/energyjsonhandler.h new file mode 100644 index 0000000..4f7fed6 --- /dev/null +++ b/plugin/energyjsonhandler.h @@ -0,0 +1,31 @@ +#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); + +signals: + void RootMeterChanged(const QVariantMap ¶ms); + + void PowerBalanceChanged(const QVariantMap ¶ms); + +private: + EnergyManager *m_energyManager = nullptr; + +}; + +#endif // ENERGYJSONHANDLER_H diff --git a/plugin/energymanagerimpl.cpp b/plugin/energymanagerimpl.cpp new file mode 100644 index 0000000..ac450fb --- /dev/null +++ b/plugin/energymanagerimpl.cpp @@ -0,0 +1,135 @@ +#include "energymanagerimpl.h" +#include "nymeasettings.h" + +#include + +Q_DECLARE_LOGGING_CATEGORY(dcEnergyExperience) + +EnergyManagerImpl::EnergyManagerImpl(ThingManager *thingManager, QObject *parent): + EnergyManager(parent), + m_thingManager(thingManager) +{ + // Most of the time we get a bunch of signals at the same time (root meter, producers, consumers etc) + // In order to decrease some load on the system, we'll wait for wee bit until we actually update to + // accumulate those changes and calculate the change in one go. + m_balanceUpdateTimer.setInterval(50); + 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; + + foreach (Thing *thing, m_thingManager->configuredThings()) { + watchThing(thing); + } + connect(thingManager, &ThingManager::thingAdded, this, &EnergyManagerImpl::watchThing); + connect(thingManager, &ThingManager::thingRemoved, this, &EnergyManagerImpl::unwatchThing); +} + +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; +} + +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()) << "Wathing thing:" << thing->name(); + if (thing->thingClass().interfaces().contains("smartmeterproducer") + || thing->thingClass().interfaces().contains("energymeter") + || thing->thingClass().interfaces().contains("energystorage")) { + connect(thing, &Thing::stateValueChanged, this, [=](const StateTypeId &stateTypeId){ + if (thing->thingClass().getStateType(stateTypeId).name() == "currentPower") { + m_balanceUpdateTimer.start(); + } + }); + } +} + +void EnergyManagerImpl::unwatchThing(const ThingId &thingId) +{ + if (m_rootMeter && m_rootMeter->id() == thingId) { + m_rootMeter = nullptr; + emit rootMeterChanged(); + } +} + +void EnergyManagerImpl::updatePowerBalance() +{ + double currentPowerAcquisition = 0; + if (m_rootMeter) { + currentPowerAcquisition = m_rootMeter->stateValue("currentPower").toDouble(); + } + + double currentPowerProduction = 0; + foreach (Thing* thing, m_thingManager->configuredThings().filterByInterface("smartmeterproducer")) { + currentPowerProduction += thing->stateValue("currentPower").toDouble(); + } + + double currentBatteryBalance = 0; + foreach (Thing *thing, m_thingManager->configuredThings().filterByInterface("energystorage")) { + currentBatteryBalance += thing->stateValue("currentPower").toDouble(); + } + + double currentPowerConsumption = -currentPowerProduction + currentPowerAcquisition - currentBatteryBalance; + + qCDebug(dcEnergyExperience()) << "Consumption:" << currentPowerConsumption << "Production:" << currentPowerProduction << "Acquisition:" << currentPowerAcquisition << "Battery:" << currentBatteryBalance; + if (currentPowerAcquisition != m_currentPowerAcquisition + || currentPowerConsumption != m_currentPowerConsumption + || currentPowerProduction != m_currentPowerProduction) { + m_currentPowerAcquisition = currentPowerAcquisition; + m_currentPowerProduction = currentPowerProduction; + m_currentPowerConsumption = currentPowerConsumption; + emit powerBalanceChanged(); + } +} + +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..1a70d90 --- /dev/null +++ b/plugin/energymanagerimpl.h @@ -0,0 +1,45 @@ +#ifndef ENERGYMANAGERIMPL_H +#define ENERGYMANAGERIMPL_H + +#include +#include +#include + +#include "integrations/thingmanager.h" + +#include "energymanager.h" + +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; + +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; + double m_currentPowerProduction; + double m_currentPowerAcquisition; +}; + +#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..68acd49 --- /dev/null +++ b/plugin/plugin.pro @@ -0,0 +1,42 @@ +TEMPLATE = lib +TARGET = $$qtLibraryTarget(nymea_experiencepluginenergy) + +CONFIG += plugin link_pkgconfig c++11 +PKGCONFIG += nymea + +QT -= gui +QT += network + +include(../config.pri) + +INCLUDEPATH += $$top_srcdir/libnymea-energy +LIBS += -L$$top_builddir/libnymea-energy -lnymea-energy + +HEADERS += experiencepluginenergy.h \ + energyjsonhandler.h \ + energymanagerimpl.h + +SOURCES += experiencepluginenergy.cpp \ + energyjsonhandler.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 + From 59d4e1d50da38323803ddf05794db28d52746439 Mon Sep 17 00:00:00 2001 From: Michael Zanetti Date: Mon, 15 Nov 2021 01:20:17 +0100 Subject: [PATCH 02/20] Add logging support --- libnymea-energy/energylogs.cpp | 108 +++++ libnymea-energy/energylogs.h | 125 ++++++ libnymea-energy/energymanager.h | 7 + libnymea-energy/libnymea-energy.pro | 2 + plugin/energyjsonhandler.cpp | 80 +++- plugin/energyjsonhandler.h | 6 +- plugin/energylogger.cpp | 637 ++++++++++++++++++++++++++++ plugin/energylogger.h | 68 +++ plugin/energymanagerimpl.cpp | 49 ++- plugin/energymanagerimpl.h | 8 + plugin/plugin.pro | 4 +- 11 files changed, 1081 insertions(+), 13 deletions(-) create mode 100644 libnymea-energy/energylogs.cpp create mode 100644 libnymea-energy/energylogs.h create mode 100644 plugin/energylogger.cpp create mode 100644 plugin/energylogger.h diff --git a/libnymea-energy/energylogs.cpp b/libnymea-energy/energylogs.cpp new file mode 100644 index 0000000..4483945 --- /dev/null +++ b/libnymea-energy/energylogs.cpp @@ -0,0 +1,108 @@ +#include "energylogs.h" + +#include + +EnergyLogs::EnergyLogs(QObject *parent): QObject(parent) +{ + +} + +PowerBalanceLogEntry::PowerBalanceLogEntry() +{ + +} + +PowerBalanceLogEntry::PowerBalanceLogEntry(const QDateTime ×tamp, double consumption, double production, double acquisition, double storage): + m_timestamp(timestamp), + m_consumption(consumption), + m_production(production), + m_acquisition(acquisition), + m_storage(storage) +{ + +} + +QDateTime PowerBalanceLogEntry::timestamp() const +{ + return m_timestamp; +} + +double PowerBalanceLogEntry::consumption() const +{ + return m_consumption; +} + +double PowerBalanceLogEntry::production() const +{ + return m_production; +} + +double PowerBalanceLogEntry::acquisition() const +{ + return m_acquisition; +} + +double PowerBalanceLogEntry::storage() const +{ + return m_storage; +} + +QVariant PowerBalanceLogEntries::get(int index) const +{ + return QVariant::fromValue(at(index)); +} + +void PowerBalanceLogEntries::put(const QVariant &variant) +{ + append(variant.value()); +} + +ThingPowerLogEntry::ThingPowerLogEntry() +{ + +} + +ThingPowerLogEntry::ThingPowerLogEntry(const QDateTime ×tamp, const ThingId &thingId, double currentPower, double totalConsumption, double totalProduction): + m_timestamp(timestamp), + m_thingId(thingId), + m_currentPower(currentPower), + m_totalConsumption(totalConsumption), + m_totalProduction(totalProduction) +{ + +} + +QDateTime ThingPowerLogEntry::timestamp() const +{ + return m_timestamp; +} + +ThingId ThingPowerLogEntry::thingId() const +{ + return m_thingId; +} + +double ThingPowerLogEntry::currentPower() const +{ + return m_currentPower; +} + +double ThingPowerLogEntry::totalConsumption() const +{ + return m_totalConsumption; +} + +double ThingPowerLogEntry::totalProduction() const +{ + return m_totalProduction; +} + +QVariant ThingPowerLogEntries::get(int index) const +{ + return QVariant::fromValue(at(index)); +} + +void ThingPowerLogEntries::put(const QVariant &variant) +{ + append(variant.value()); +} diff --git a/libnymea-energy/energylogs.h b/libnymea-energy/energylogs.h new file mode 100644 index 0000000..9f92ae9 --- /dev/null +++ b/libnymea-energy/energylogs.h @@ -0,0 +1,125 @@ +#ifndef ENERGYLOGS_H +#define ENERGYLOGS_H + +#include +#include +#include + +#include + +class PowerBalanceLogEntry; +class PowerBalanceLogEntries; +class ThingPowerLogEntry; +class ThingPowerLogEntries; + +class EnergyLogs: public QObject +{ + Q_OBJECT +public: + EnergyLogs(QObject *parent = nullptr); + virtual ~EnergyLogs() = default; + + enum SampleRate { + SampleRate1Min = 1, + SampleRate15Mins = 15, + SampleRate1Hour = 60, + SampleRate3Hours = 180, + SampleRate1Day = 1440, + SampleRate1Week = 10080, + SampleRate1Month = 43200, + SampleRate1Year = 525600 + }; + Q_ENUM(SampleRate) + + /*! Returns logs for the given sample rate for total household consumption, production, acquisition and storage balance. + * From and to may be given to limit results to a time span. + */ + virtual PowerBalanceLogEntries powerBalanceLogs(SampleRate sampleRate, const QDateTime &from = QDateTime(), const QDateTime &to = QDateTime()) const = 0; + + /*! Returns logs for the given sample rate for currentPower, totalEnergyConsumed and totalEnergyProduced for the given things. + * From and to may be given to limie results to a time span. + * If thingIds is empty, all things will be returned. + */ + virtual ThingPowerLogEntries thingPowerLogs(SampleRate sampleRate, const QList &thingIds, const QDateTime &from = QDateTime(), const QDateTime &to = QDateTime()) const = 0; + +signals: + void powerBalanceEntryAdded(SampleRate sampleRate, const PowerBalanceLogEntry &entry); + void thingPowerEntryAdded(SampleRate sampleRate, const ThingPowerLogEntry &entry); + +}; + + +class PowerBalanceLogEntry +{ + Q_GADGET + Q_PROPERTY(QDateTime timestamp READ timestamp) + Q_PROPERTY(double consumption READ consumption) + Q_PROPERTY(double production READ production) + Q_PROPERTY(double acquisition READ acquisition) + Q_PROPERTY(double storage READ storage) +public: + PowerBalanceLogEntry(); + PowerBalanceLogEntry(const QDateTime ×tamp, double consumption, double production, double acquisition, double storage); + QDateTime timestamp() const; + double consumption() const; + double production() const; + double acquisition() const; + double storage() const; +private: + QDateTime m_timestamp; + double m_consumption = 0; + double m_production = 0; + double m_acquisition = 0; + double m_storage = 0; +}; +Q_DECLARE_METATYPE(PowerBalanceLogEntry) + +class PowerBalanceLogEntries: public QList +{ + Q_GADGET + Q_PROPERTY(int count READ count) +public: + PowerBalanceLogEntries() = default; + PowerBalanceLogEntries(const QList &other); + Q_INVOKABLE QVariant get(int index) const; + Q_INVOKABLE void put(const QVariant &variant); +}; +Q_DECLARE_METATYPE(PowerBalanceLogEntries) + +class ThingPowerLogEntry { + Q_GADGET + Q_PROPERTY(QDateTime timstamp READ timestamp) + Q_PROPERTY(QUuid thingId READ thingId) + Q_PROPERTY(double currentPower READ currentPower) + Q_PROPERTY(double totalConsumption READ totalConsumption) + Q_PROPERTY(double totalProduction READ totalProduction) +public: + ThingPowerLogEntry(); + ThingPowerLogEntry(const QDateTime ×tamp, const ThingId &thingId, double currentPower, double totalConsumption, double totalProuction); + QDateTime timestamp() const; + ThingId thingId() const; + double currentPower() const; + double totalConsumption() const; + double totalProduction() const; +private: + QDateTime m_timestamp; + ThingId m_thingId; + double m_currentPower = 0; + double m_totalConsumption = 0; + double m_totalProduction = 0; +}; +Q_DECLARE_METATYPE(ThingPowerLogEntry) + +class ThingPowerLogEntries: public QList +{ + Q_GADGET + Q_PROPERTY(int count READ count) +public: + ThingPowerLogEntries() = default; + ThingPowerLogEntries(const QList &other): QList(other) {} + Q_INVOKABLE QVariant get(int index) const; + Q_INVOKABLE void put(const QVariant &variant); +}; +Q_DECLARE_METATYPE(ThingPowerLogEntries) + +#endif // ENERGYLOGS_H diff --git a/libnymea-energy/energymanager.h b/libnymea-energy/energymanager.h index 83ce535..8a560cd 100644 --- a/libnymea-energy/energymanager.h +++ b/libnymea-energy/energymanager.h @@ -32,10 +32,13 @@ #ifndef ENERGYMANAGER_H #define ENERGYMANAGER_H +#include "energylogs.h" + #include #include + class EnergyManager : public QObject { Q_OBJECT @@ -47,6 +50,7 @@ public: }; Q_ENUM(EnergyError) + explicit EnergyManager(QObject *parent = nullptr); virtual ~EnergyManager() = default; @@ -56,6 +60,9 @@ public: virtual double currentPowerConsumption() const = 0; virtual double currentPowerProduction() const = 0; virtual double currentPowerAcquisition() const = 0; + virtual double currentPowerStorage() const = 0; + + virtual EnergyLogs* logs() const = 0; signals: void rootMeterChanged(); diff --git a/libnymea-energy/libnymea-energy.pro b/libnymea-energy/libnymea-energy.pro index fb31ade..91e8370 100644 --- a/libnymea-energy/libnymea-energy.pro +++ b/libnymea-energy/libnymea-energy.pro @@ -9,10 +9,12 @@ PKGCONFIG += nymea HEADERS += \ + energylogs.h \ energymanager.h \ energyplugin.h SOURCES += \ + energylogs.cpp \ energymanager.cpp \ energyplugin.cpp diff --git a/plugin/energyjsonhandler.cpp b/plugin/energyjsonhandler.cpp index c6ee7d9..22e402a 100644 --- a/plugin/energyjsonhandler.cpp +++ b/plugin/energyjsonhandler.cpp @@ -8,6 +8,10 @@ EnergyJsonHandler::EnergyJsonHandler(EnergyManager *energyManager, QObject *pare m_energyManager(energyManager) { registerEnum(); + registerEnum(); + + registerObject(); + registerObject(); QVariantMap params, returns; QString description; @@ -30,18 +34,51 @@ EnergyJsonHandler::EnergyJsonHandler(EnergyManager *energyManager, QObject *pare returns.insert("currentPowerAcquisition", enumValueName(Double)); registerMethod("GetPowerBalance", description, params, returns); + params.clear(); returns.clear(); + description = "Get logs for the power balance. If from is not give, the log will start at the beginning of " + "recording. If to is not given, the logs will and at the last sample for this sample rate before now."; + params.insert("sampleRate", enumRef()); + params.insert("o:from", enumValueName(Uint)); + params.insert("o:to", enumValueName(Uint)); + returns.insert("powerBalanceLogEntries", objectRef()); + registerMethod("GetPowerBalanceLogs", description, params, returns); + + params.clear(); returns.clear(); + description = "Get logs for one or more things power values. If thingIds is not given, logs for all energy related " + "things will be returned. If from is not given, the log will start at the beginning of recording. If " + "to is not given, the logs will and at the last sample for this sample rate before now."; + params.insert("sampleRate", enumRef()); + params.insert("o:thingIds", QVariantList() << enumValueName(Uuid)); + params.insert("o:from", enumValueName(Uint)); + params.insert("o:to", enumValueName(Uint)); + returns.insert("thingPowerLogEntries", objectRef()); + registerMethod("GetThingPowerLogs", description, params, returns); + params.clear(); description = "Emitted whenever the root meter id changes. If the root meter has been unset, the params will be empty."; params.insert("o:rootMeterThingId", enumValueName(Uuid)); registerNotification("RootMeterChanged", description, params); params.clear(); - description = "Emitted whenever the energy balance changes. That is, when the current consumption, production or acquisition changes. Typically they will all change at the same time."; + description = "Emitted whenever the energy balance changes. That is, when the current consumption, production or " + "acquisition changes. Typically they will all change at the same time."; params.insert("currentPowerConsumption", enumValueName(Double)); params.insert("currentPowerProduction", enumValueName(Double)); params.insert("currentPowerAcquisition", enumValueName(Double)); registerNotification("PowerBalanceChanged", description, params); + params.clear(); + description = "Emitted whenever a entry is added to the power balance log."; + params.insert("sampleRate", enumRef()); + params.insert("powerBalanceLogEntry", objectRef()); + registerNotification("PowerBalanceLogEntryAdded", description, params); + + params.clear(); + description = "Emitted whenever a entry is added to the thing power log."; + params.insert("sampleRate", enumRef()); + params.insert("thingPowerLogEntry", objectRef()); + registerNotification("ThingPowerLogEntryAdded", description, params); + connect(m_energyManager, &EnergyManager::rootMeterChanged, this, [=](){ QVariantMap params; if (m_energyManager->rootMeter()) { @@ -57,6 +94,20 @@ EnergyJsonHandler::EnergyJsonHandler(EnergyManager *energyManager, QObject *pare params.insert("currentPowerAcquisition", m_energyManager->currentPowerAcquisition()); emit PowerBalanceChanged(params); }); + + connect(m_energyManager->logs(), &EnergyLogs::powerBalanceEntryAdded, this, [=](EnergyLogs::SampleRate sampleRate, const PowerBalanceLogEntry &entry){ + QVariantMap params; + params.insert("sampleRate", enumValueName(sampleRate)); + params.insert("powerBalanceLogEntry", pack(entry)); + emit PowerBalanceLogEntryAdded(params); + }); + + connect(m_energyManager->logs(), &EnergyLogs::thingPowerEntryAdded, this, [=](EnergyLogs::SampleRate sampleRate, const ThingPowerLogEntry &entry){ + QVariantMap params; + params.insert("sampleRate", enumValueName(sampleRate)); + params.insert("thingPowerLogEntry", pack(entry)); + emit ThingPowerLogEntryAdded(params); + }); } QString EnergyJsonHandler::name() const @@ -96,3 +147,30 @@ JsonReply *EnergyJsonHandler::GetPowerBalance(const QVariantMap ¶ms) ret.insert("currentPowerAcquisition", m_energyManager->currentPowerAcquisition()); return createReply(ret); } + +JsonReply *EnergyJsonHandler::GetPowerBalanceLogs(const QVariantMap ¶ms) +{ + qCDebug(dcEnergyExperience()) << "params" << params; + qCDebug(dcEnergyExperience()) << "from" << params.value("from"); + EnergyLogs::SampleRate sampleRate = enumNameToValue(params.value("sampleRate").toString()); + QDateTime from = params.contains("from") ? QDateTime::fromMSecsSinceEpoch(params.value("from").toLongLong() * 1000) : QDateTime(); + qCDebug(dcEnergyExperience()) << "from2" << from; + QDateTime to = params.contains("to") ? QDateTime::fromMSecsSinceEpoch(params.value("to").toLongLong() * 1000) : QDateTime(); + QVariantMap returns; + returns.insert("powerBalanceLogEntries", pack(m_energyManager->logs()->powerBalanceLogs(sampleRate, from, to))); + return createReply(returns); +} + +JsonReply *EnergyJsonHandler::GetThingPowerLogs(const QVariantMap ¶ms) +{ + EnergyLogs::SampleRate sampleRate = enumNameToValue(params.value("sampleRate").toString()); + QList thingIds; + foreach (const QVariant &thingId, params.value("thingIds").toList()) { + thingIds.append(thingId.toUuid()); + } + QDateTime from = params.contains("from") ? QDateTime::fromMSecsSinceEpoch(params.value("from").toLongLong() * 1000) : QDateTime(); + QDateTime to = params.contains("to") ? QDateTime::fromMSecsSinceEpoch(params.value("to").toLongLong() * 1000) : QDateTime(); + QVariantMap returns; + returns.insert("thingPowerLogEntries",pack(m_energyManager->logs()->thingPowerLogs(sampleRate, thingIds, from, to))); + return createReply(returns); +} diff --git a/plugin/energyjsonhandler.h b/plugin/energyjsonhandler.h index 4f7fed6..4d42b35 100644 --- a/plugin/energyjsonhandler.h +++ b/plugin/energyjsonhandler.h @@ -17,15 +17,17 @@ public: Q_INVOKABLE JsonReply* GetRootMeter(const QVariantMap ¶ms); Q_INVOKABLE JsonReply* SetRootMeter(const QVariantMap ¶ms); Q_INVOKABLE JsonReply* GetPowerBalance(const QVariantMap ¶ms); + Q_INVOKABLE JsonReply* GetPowerBalanceLogs(const QVariantMap ¶ms); + Q_INVOKABLE JsonReply* GetThingPowerLogs(const QVariantMap ¶ms); signals: void RootMeterChanged(const QVariantMap ¶ms); - void PowerBalanceChanged(const QVariantMap ¶ms); + void PowerBalanceLogEntryAdded(const QVariantMap ¶ms); + void ThingPowerLogEntryAdded(const QVariantMap ¶ms); private: EnergyManager *m_energyManager = nullptr; - }; #endif // ENERGYJSONHANDLER_H diff --git a/plugin/energylogger.cpp b/plugin/energylogger.cpp new file mode 100644 index 0000000..6ff2ef2 --- /dev/null +++ b/plugin/energylogger.cpp @@ -0,0 +1,637 @@ +#include "energylogger.h" + +#include + +#include +#include +#include +#include +#include + +#include +Q_DECLARE_LOGGING_CATEGORY(dcEnergyExperience) + +EnergyLogger::EnergyLogger(QObject *parent) : EnergyLogs(parent) +{ + if (!initDB()) { + qCCritical(dcEnergyExperience()) << "Unable to open energy log. Energy logs will not be available."; + return; + } + + // Logging configuration + // Note: SampleRate1Min is always sampled as it is the base series for others + // Make sure your base series always has enough samples to build a full sample + // of all series building on it. + + // Disk space considerations; + // Each entry takes approx 30 bytes for powerBalance + 50 bytes for thingCurrentPower per thing of disk space + // SQLite adds metadata and overhead of about 5% + // The resulting database size can be estimated with (count being the sum of all numbers below): + // (count * 30 bytes) + (count * things * 50 bytes) + 5% + // 10000 entries, with 5 energy things => ~3MB + + m_maxMinuteSamples = 15; + + addConfig(SampleRate15Mins, SampleRate1Min, 6720); // 10 weeks + addConfig(SampleRate1Hour, SampleRate15Mins, 1680); // 10 weeks + addConfig(SampleRate3Hours, SampleRate15Mins, 560); // 10 weeks + addConfig(SampleRate1Day, SampleRate1Hour, 1095); // 3 years + addConfig(SampleRate1Week, SampleRate1Day, 168); // 3 years + addConfig(SampleRate1Month, SampleRate1Day, 240); // 20 years + addConfig(SampleRate1Year, SampleRate1Month, 20); // 20 years + + // Load thingIds from logs so we have the complete list available for sampling, even if a thing might not produce any logs for a while. + QSqlQuery query(m_db); + query.prepare("SELECT DISTINCT thingId FROM thingPower;"); + query.exec(); + if (query.lastError().isValid()) { + qCWarning(dcEnergyExperience()) << "Failed to load existing things from logs:" << query.lastError(); + } else { + while (query.next()) { + m_thingsPowerLiveLogs[query.value("thingId").toUuid()] = ThingPowerLogEntries(); + } + } + + // Start the scheduling + + scheduleNextSample(SampleRate1Min); + foreach (SampleRate sampleRate, m_configs.keys()) { + scheduleNextSample(sampleRate); + } + + // Now all the data is initialized. We can start with sampling. + + // First check if we missed any samplings (e.g. because the system was offline at the time when it should have created a sample) + foreach(SampleRate sampleRate, m_configs.keys()) { + rectifySamples(sampleRate, m_configs.value(sampleRate).baseSampleRate); + } + + // And start the sampler timer + connect(&m_sampleTimer, &QTimer::timeout, this, &EnergyLogger::sample); + m_sampleTimer.start(1000); +} + +void EnergyLogger::logPowerBalance(double consumption, double production, double acquisition, double storage) +{ + PowerBalanceLogEntry entry(QDateTime::currentDateTime(), consumption, production, acquisition, storage); + + // Add everything to livelog, keep that for one day, in memory only + m_balanceLiveLog.prepend(entry); + while (m_balanceLiveLog.count() > 1 && m_balanceLiveLog.last().timestamp().addDays(1) < QDateTime::currentDateTime()) { + qCDebug(dcEnergyExperience) << "Discarding livelog entry from" << m_balanceLiveLog.last().timestamp().toString(); + m_balanceLiveLog.removeLast(); + } +} + +void EnergyLogger::logThingPower(const ThingId &thingId, double currentPower, double totalConsumption, double totalProduction) +{ + qCDebug(dcEnergyExperience()) << "Logging thing power:" << currentPower << totalConsumption << totalProduction; + ThingPowerLogEntry entry(QDateTime::currentDateTime(), thingId, currentPower, totalConsumption, totalProduction); + + m_thingsPowerLiveLogs[thingId].prepend(entry); + while (m_thingsPowerLiveLogs[thingId].count() > 1 && m_thingsPowerLiveLogs[thingId].last().timestamp().addDays(1) < QDateTime::currentDateTime()) { + qCDebug(dcEnergyExperience()) << "Discarding thing power livelog entry for thing" << thingId << "from" << m_thingsPowerLiveLogs[thingId].last().timestamp().toString(); + m_thingsPowerLiveLogs[thingId].removeLast(); + } +} +PowerBalanceLogEntries EnergyLogger::powerBalanceLogs(SampleRate sampleRate, const QDateTime &from, const QDateTime &to) const +{ + PowerBalanceLogEntries result; + + QSqlQuery query(m_db); + QString queryString = "SELECT * FROM powerBalance WHERE sampleRate = ?"; + QVariantList bindValues; + bindValues << sampleRate; + qCDebug(dcEnergyExperience()) << "Fetching logs. Timestamp:" << from << from.isNull(); + if (!from.isNull()) { + queryString += " AND timestamp >= ?"; + bindValues << from.toMSecsSinceEpoch(); + } + if (!to.isNull()) { + queryString += " AND timestamp <= ?"; + bindValues << to.toMSecsSinceEpoch(); + } + query.prepare(queryString); + foreach (const QVariant &bindValue, bindValues) { + query.addBindValue(bindValue); + } + + qCDebug(dcEnergyExperience()) << "Executing" << queryString << query.executedQuery() << bindValues; + query.exec(); + if (query.lastError().isValid()) { + qCWarning(dcEnergyExperience()) << "Error fetching power balance logs:" << query.lastError() << query.executedQuery(); + return result; + } + + while (query.next()) { + qCDebug(dcEnergyExperience()) << "Adding result"; + result.append(PowerBalanceLogEntry(QDateTime::fromMSecsSinceEpoch(query.value("timestamp").toLongLong()), query.value("consumption").toDouble(), query.value("production").toDouble(), query.value("acquisition").toDouble(), query.value("storage").toDouble())); + } + return result; +} + +ThingPowerLogEntries EnergyLogger::thingPowerLogs(SampleRate sampleRate, const QList &thingIds, const QDateTime &from, const QDateTime &to) const +{ + ThingPowerLogEntries result; + + QSqlQuery query(m_db); + QString queryString = "SELECT * FROM thingPower WHERE sampleRate = ?"; + QVariantList bindValues; + bindValues << sampleRate; + + QStringList thingsQuery; + foreach (const ThingId &thingId, thingIds) { + thingsQuery.append("thingId = ?"); + bindValues << thingId; + } + if (!thingsQuery.isEmpty()) { + queryString += "AND (" + thingsQuery.join(" OR ") + " )"; + } + + if (!from.isNull()) { + queryString += " AND timestamp >= ?"; + bindValues << from.toMSecsSinceEpoch(); + } + if (!to.isNull()) { + queryString += " AND timestamp <= ?"; + bindValues << to.toMSecsSinceEpoch(); + } + query.prepare(queryString); + foreach (const QVariant &bindValue, bindValues) { + query.addBindValue(bindValue); + } + query.exec(); + if (query.lastError().isValid()) { + qCWarning(dcEnergyExperience()) << "Error fetching power balance logs:" << query.lastError() << query.executedQuery(); + return result; + } + + while (query.next()) { + result.append(ThingPowerLogEntry( + QDateTime::fromMSecsSinceEpoch(query.value("timestamp").toLongLong()), + query.value("thingId").toUuid(), + query.value("currentPower").toDouble(), + query.value("totalConsumption").toDouble(), + query.value("totalProduction").toDouble())); + } + return result; + +} + +void EnergyLogger::sample() +{ + QDateTime now = QDateTime::currentDateTime(); + + if (now >= m_nextSamples.value(SampleRate1Min)) { + QDateTime sampleEnd = m_nextSamples.value(SampleRate1Min); + QDateTime sampleStart = sampleEnd.addMSecs(-60 * 1000); + + qCDebug(dcEnergyExperience()) << "Sampling 1 min" << sampleEnd.toString(); + + double medianConsumption = 0; + double medianProduction = 0; + double medianAcquisition = 0; + double medianStorage = 0; + for (int i = 0; i < m_balanceLiveLog.count(); i++) { + const PowerBalanceLogEntry &entry = m_balanceLiveLog.at(i); + QDateTime frameStart = (entry.timestamp() < sampleStart) ? sampleStart : entry.timestamp(); + QDateTime frameEnd = i == 0 ? sampleEnd : m_balanceLiveLog.at(i-1).timestamp(); + int frameDuration = frameStart.msecsTo(frameEnd); + medianConsumption += entry.consumption() * frameDuration; + medianProduction += entry.production() * frameDuration; + medianAcquisition += entry.acquisition() * frameDuration; + medianStorage += entry.storage() * frameDuration; +// qCDebug(dcEnergyExperience()) << "Frame" << i << "duration:" << frameDuration << "value:" << entry.consumption << "start" << frameStart.toString() << "end" << frameEnd.toString(); + if (entry.timestamp() < sampleStart) { + break; + } + } + medianConsumption /= sampleStart.msecsTo(sampleEnd); + medianProduction /= sampleStart.msecsTo(sampleEnd); + medianAcquisition /= sampleStart.msecsTo(sampleEnd); + medianStorage /= sampleStart.msecsTo(sampleEnd); + qCDebug(dcEnergyExperience()) << "Power balance for sample:" << medianConsumption << medianProduction << medianAcquisition << medianStorage << "duration:" << sampleStart.msecsTo(sampleEnd); + insertPowerBalance(sampleEnd, SampleRate1Min, medianConsumption, medianProduction, medianAcquisition, medianStorage); + + foreach (const ThingId &thingId, m_thingsPowerLiveLogs.keys()) { + medianConsumption = 0; + ThingPowerLogEntries entries = m_thingsPowerLiveLogs.value(thingId); + for (int i = 0; i < entries.count(); i++) { + const ThingPowerLogEntry &entry = entries.at(i); + QDateTime frameStart = (entry.timestamp() < sampleStart) ? sampleStart : entry.timestamp(); + QDateTime frameEnd = i == 0 ? sampleEnd : entries.at(i-1).timestamp(); + int frameDuration = frameStart.msecsTo(frameEnd); + medianConsumption += entry.currentPower() * frameDuration; +// qCDebug(dcEnergyExperience()) << "Frame" << i << "duration:" << frameDuration << "value:" << entry.value; + if (entry.timestamp() < sampleStart) { + break; + } + } + medianConsumption /= sampleStart.msecsTo(sampleEnd); + double totalConsumption = 0; + double totalProduction = 0; + if (entries.count() > 0) { + totalConsumption = entries.last().totalConsumption(); + totalProduction = entries.last().totalProduction(); + } + qCDebug(dcEnergyExperience()) << "Thing power of sample:" << medianConsumption << totalConsumption << totalProduction << "total duration:" << sampleStart.msecsTo(sampleEnd); + insertThingPower(sampleEnd, SampleRate1Min, thingId, medianConsumption, totalConsumption, totalProduction); + } + } + + // First sample all the configs. + foreach (SampleRate sampleRate, m_configs.keys()) { + if (now >= m_nextSamples.value(sampleRate)) { + QDateTime sampleTime = m_nextSamples.value(sampleRate); + SampleRate baseSampleRate = m_configs.value(sampleRate).baseSampleRate; + samplePowerBalance(sampleRate, baseSampleRate, sampleTime); + sampleThingsPower(sampleRate, baseSampleRate, sampleTime); + } + } + + // and then trim them + if (now > m_nextSamples.value(SampleRate1Min)) { + QDateTime sampleTime = m_nextSamples.value(SampleRate1Min); + QDateTime oldestTimestamp = sampleTime.addMSecs(-m_maxMinuteSamples * 60 * 1000); + trimPowerBalance(SampleRate1Min, oldestTimestamp); + foreach (const ThingId &thingId, m_thingsPowerLiveLogs.keys()) { + trimThingPower(thingId, SampleRate1Min, oldestTimestamp); + } + } + foreach (SampleRate sampleRate, m_configs.keys()) { + if (now >= m_nextSamples.value(sampleRate)) { + QDateTime sampleTime = m_nextSamples.value(sampleRate); + QDateTime oldestTimestamp = sampleTime.addMSecs(-m_configs.value(sampleRate).maxSamples * sampleRate * 60 * 1000); + trimPowerBalance(sampleRate, oldestTimestamp); + foreach (const ThingId &thingId, m_thingsPowerLiveLogs.keys()) { + trimThingPower(thingId, sampleRate, oldestTimestamp); + } + } + } + + // Lastly we reschedule the next sample for each config + // Note: keep this at the end as the previous stuff uses the schedule to work + if (now > m_nextSamples.value(SampleRate1Min)) { + scheduleNextSample(SampleRate1Min); + } + foreach (SampleRate sampleRate, m_configs.keys()) { + if (now >= m_nextSamples.value(sampleRate)) { + scheduleNextSample(sampleRate); + } + } +} + +bool EnergyLogger::initDB() +{ + m_db.close(); + + m_db = QSqlDatabase::addDatabase("QSQLITE", "energylogs"); + QDir path = QDir(NymeaSettings::storagePath()); + if (!path.exists()) { + path.mkpath(path.path()); + } + m_db.setDatabaseName(path.filePath("energylogs.sqlite")); + + bool opened = m_db.open(); + if (!opened) { + qCWarning(dcEnergyExperience()) << "Cannot open energy log DB at" << m_db.databaseName() << m_db.lastError(); + return false; + } + + if (!m_db.tables().contains("powerBalance")) { + qCDebug(dcEnergyExperience()) << "No \"powerBalance\" table in database. Creating it."; + m_db.exec("CREATE TABLE powerBalance " + "(" + "timestamp BIGINT," + "sampleRate INT," + "consumption FLOAT," + "production FLOAT," + "acquisition FLOAT," + "storage FLOAT" + ");"); + + if (m_db.lastError().isValid()) { + qCWarning(dcEnergyExperience()) << "Error creating powerBalance table in energy log database. Driver error:" << m_db.lastError().driverText() << "Database error:" << m_db.lastError().databaseText(); + return false; + } + } + + if (!m_db.tables().contains("thingPower")) { + qCDebug(dcEnergyExperience()) << "No \"thingPower\" table in database. Creating it."; + m_db.exec("CREATE TABLE thingPower " + "(" + "timestamp BIGINT," + "sampleRate INT," + "thingId VARCHAR(38)," + "currentPower FLOAT," + "totalConsumption FLOAT," + "totalProduction FLOAT" + ");"); + if (m_db.lastError().isValid()) { + qCWarning(dcEnergyExperience()) << "Error creating thingPower table in energy log database. Driver error:" << m_db.lastError().driverText() << "Database error:" << m_db.lastError().databaseText(); + return false; + } + } + + qCDebug(dcEnergyExperience()) << "Initialized logging DB successfully."; + return true; +} + +void EnergyLogger::addConfig(SampleRate sampleRate, SampleRate baseSampleRate, int maxSamples) +{ + SampleConfig config; + config.baseSampleRate = baseSampleRate; + config.maxSamples = maxSamples; + m_configs.insert(sampleRate, config); +} + +QDateTime EnergyLogger::getOldestPowerBalanceSampleTimestamp(SampleRate sampleRate) +{ + QSqlQuery query(m_db); + query.prepare("SELECT MIN(timestamp) AS oldestTimestamp FROM powerBalance WHERE sampleRate = ?;"); + query.addBindValue(sampleRate); + query.exec(); + if (query.next() && !query.value("oldestTimestamp").isNull()) { + return QDateTime::fromMSecsSinceEpoch(query.value("oldestTimestamp").toLongLong()); + } + return QDateTime(); +} + +QDateTime EnergyLogger::getNewestPowerBalanceSampleTimestamp(SampleRate sampleRate) +{ + QSqlQuery query(m_db); + query.prepare("SELECT MAX(timestamp) AS latestTimestamp FROM powerBalance WHERE sampleRate = ?;"); + query.addBindValue(sampleRate); + query.exec(); + if (query.next() && !query.value("latestTimestamp").isNull()) { + return QDateTime::fromMSecsSinceEpoch(query.value("latestTimestamp").toLongLong()); + } + return QDateTime(); +} + +QDateTime EnergyLogger::getOldestThingPowerSampleTimestamp(const ThingId &thingId, SampleRate sampleRate) +{ + QSqlQuery query(m_db); + query.prepare("SELECT MIN(timestamp) AS oldestTimestamp FROM thingPower WHERE thingId = ? AND sampleRate = ?;"); + query.addBindValue(thingId); + query.addBindValue(sampleRate); + query.exec(); + if (query.next() && !query.value("oldestTimestamp").isNull()) { + return QDateTime::fromMSecsSinceEpoch(query.value("oldestTimestamp").toLongLong()); + } + return QDateTime(); +} + +QDateTime EnergyLogger::getNewestThingPowerSampleTimestamp(const ThingId &thingId, SampleRate sampleRate) +{ + QSqlQuery query(m_db); + query.prepare("SELECT MAX(timestamp) AS newestTimestamp FROM thingPower WHERE thingId = ? AND sampleRate = ?;"); + query.addBindValue(thingId); + query.addBindValue(sampleRate); + query.exec(); + if (query.next() && !query.value("newestTimestamp").isNull()) { + return QDateTime::fromMSecsSinceEpoch(query.value("newestTimestamp").toLongLong()); + } + return QDateTime(); +} + +void EnergyLogger::scheduleNextSample(SampleRate sampleRate) +{ + QDateTime next = nextSampleTimestamp(sampleRate, QDateTime::currentDateTime()); + m_nextSamples.insert(sampleRate, next); + qCDebug(dcEnergyExperience()) << "Next sample for" << sampleRate << "scheduled at" << next.toString(); +} + +void EnergyLogger::rectifySamples(SampleRate sampleRate, SampleRate baseSampleRate) +{ + // Normally we'd need to find the newest available sample of a serien and catch up from there. + // However, it could happen a series does not have any samples at all yet. For example if we're logging since january, + // and at new years the system was off, we missed the new years yearly sample and don't have any earlier. For those cases + // we need to start resampling from the oldest timestamp we find in the DB at all (regardless of the sampleRate) + QDateTime oldestBaseSample = getOldestPowerBalanceSampleTimestamp(baseSampleRate); + QDateTime newestSample = getNewestPowerBalanceSampleTimestamp(sampleRate); + + qCDebug(dcEnergyExperience()) << "Checking for missing samples for" << sampleRate; + qCDebug(dcEnergyExperience()) << "Newest sample:" << newestSample.toString() << "Oldest base sample:" << oldestBaseSample.toString(); + if (newestSample.isNull()) { + qCDebug(dcEnergyExperience()) << "No sample at all so far. Using base as starting point."; + newestSample = oldestBaseSample; + } + qCDebug(dcEnergyExperience()) << "next sample after last in series:" << nextSampleTimestamp(sampleRate, newestSample).toString(); + qCDebug(dcEnergyExperience()) << "next scheduled sample:" << m_nextSamples.value(sampleRate).toString(); + while (!newestSample.isNull() && nextSampleTimestamp(sampleRate, newestSample) < m_nextSamples[sampleRate]) { + QDateTime nextSample = nextSampleTimestamp(sampleRate, newestSample.addMSecs(1000)); + qCDebug(dcEnergyExperience()) << "Rectifying missed sample for" << sampleRate << "from" << nextSample.toString(); + samplePowerBalance(sampleRate, baseSampleRate, nextSample); + newestSample = nextSample; + } + + foreach (const ThingId &thingId, m_thingsPowerLiveLogs.keys()) { + QDateTime oldestBaseSample = getOldestThingPowerSampleTimestamp(thingId, baseSampleRate); + QDateTime newestSample = getNewestThingPowerSampleTimestamp(thingId, sampleRate); + + qCDebug(dcEnergyExperience()) << "T Checking for missing samples for" << sampleRate; + qCDebug(dcEnergyExperience()) << "T Newest sample:" << newestSample.toString() << "Oldest base sample:" << oldestBaseSample.toString(); + if (newestSample.isNull()) { + qCDebug(dcEnergyExperience()) << "T No sample at all so far. Using base as starting point."; + newestSample = oldestBaseSample; + } + qCDebug(dcEnergyExperience()) << "T next sample after last in series:" << nextSampleTimestamp(sampleRate, newestSample).toString(); + qCDebug(dcEnergyExperience()) << "T next scheduled sample:" << m_nextSamples.value(sampleRate).toString(); + while (!newestSample.isNull() && nextSampleTimestamp(sampleRate, newestSample) < m_nextSamples[sampleRate]) { + QDateTime nextSample = nextSampleTimestamp(sampleRate, newestSample.addMSecs(1000)); + qCDebug(dcEnergyExperience()) << "T Rectifying missed sample for" << sampleRate << "from" << nextSample.toString(); + sampleThingPower(thingId, sampleRate, baseSampleRate, nextSample); + newestSample = nextSample; + } + } +} + +QDateTime EnergyLogger::nextSampleTimestamp(SampleRate sampleRate, const QDateTime &dateTime) +{ + QTime time = dateTime.time(); + QDate date = dateTime.date(); + QDateTime next; + switch (sampleRate) { + case SampleRate1Min: + time.setHMS(time.hour(), time.minute(), 0); + next = QDateTime(date, time).addMSecs(60 * 1000); + break; + case SampleRate15Mins: + time.setHMS(time.hour(), time.minute() - (time.minute() % 15), 0); + next = QDateTime(date, time).addMSecs(15 * 60 * 1000); + break; + case SampleRate1Hour: + time.setHMS(time.hour(), 0, 0); + next = QDateTime(date, time).addMSecs(60 * 60 * 1000); + break; + case SampleRate3Hours: + time.setHMS(time.hour() - (time.hour() % 3), 0, 0); + next = QDateTime(date, time).addMSecs(3 * 60 * 60 * 1000); + break; + case SampleRate1Day: + next = QDateTime(date, QTime()).addDays(1); + break; + case SampleRate1Week: + date = date.addDays(-date.dayOfWeek() + 1); + next = QDateTime(date, QTime()).addDays(7); + break; + case SampleRate1Month: + date = date.addDays(-date.day() + 1); + next = QDateTime(date, QTime()).addMonths(1); + break; + case SampleRate1Year: + date.setDate(date.year(), 1, 1); + next = QDateTime(date, QTime()).addYears(1); + break; + } + + return next; +} + +bool EnergyLogger::samplePowerBalance(SampleRate sampleRate, SampleRate baseSampleRate, const QDateTime &sampleEnd) +{ + QDateTime sampleStart = sampleEnd.addMSecs(-sampleRate * 60 * 1000); + + qCDebug(dcEnergyExperience()) << "Sampling" << sampleRate << "from" << sampleStart << "to" << sampleEnd; + + QSqlQuery query(m_db); + query.prepare("SELECT * FROM powerBalance WHERE sampleRate = ? AND timestamp >= ? AND timestamp < ?;"); + query.addBindValue(baseSampleRate); + query.addBindValue(sampleStart.toMSecsSinceEpoch()); + query.addBindValue(sampleEnd.toMSecsSinceEpoch()); + query.exec(); + + if (query.lastError().isValid()) { + qCWarning(dcEnergyExperience()) << "Error fetching power balance samples for" << baseSampleRate << "from" << sampleStart.toString() << "to" << sampleEnd.toString(); + qCWarning(dcEnergyExperience()) << "SQL error was:" << query.lastError() << "executed query:" << query.executedQuery(); + return false; + } + + double medianConsumption = 0; + double medianProduction = 0; + double medianAcquisition = 0; + double medianStorage = 0; + while (query.next()) { + qCDebug(dcEnergyExperience()) << "Frame:" << query.value("consumption").toDouble() << query.value("production").toDouble() << query.value("acquisition").toDouble() << QDateTime::fromMSecsSinceEpoch(query.value("timestamp").toLongLong()).toString(); + medianConsumption += query.value("consumption").toDouble(); + medianProduction += query.value("production").toDouble(); + medianAcquisition += query.value("acquisition").toDouble(); + medianStorage += query.value("storage").toDouble(); + } + qCDebug(dcEnergyExperience()) << "Totals:" << medianConsumption << medianProduction << medianAcquisition << medianStorage << "base samplerate" << baseSampleRate << "samplerate:" << sampleRate; + medianConsumption = medianConsumption * baseSampleRate / sampleRate; + medianProduction = medianProduction * baseSampleRate / sampleRate; + medianAcquisition = medianAcquisition * baseSampleRate / sampleRate; + medianStorage = medianStorage * baseSampleRate / sampleRate; + + qCDebug(dcEnergyExperience()) << "Sampled:" << medianConsumption << medianProduction << medianAcquisition << medianStorage; + return insertPowerBalance(sampleEnd, sampleRate, medianConsumption, medianProduction, medianAcquisition, medianStorage); +} + +bool EnergyLogger::insertPowerBalance(const QDateTime ×tamp, SampleRate sampleRate, double consumption, double production, double acquisition, double storage) +{ + QSqlQuery query = QSqlQuery(m_db); + query.prepare("INSERT INTO powerBalance (timestamp, sampleRate, consumption, production, acquisition, storage) values (?, ?, ?, ?, ?, ?);"); + query.addBindValue(timestamp.toMSecsSinceEpoch()); + query.addBindValue(sampleRate); + query.addBindValue(consumption); + query.addBindValue(production); + query.addBindValue(acquisition); + query.addBindValue(storage); + query.exec(); + if (query.lastError().isValid()) { + qCWarning(dcEnergyExperience()) << "Error logging consumption sample:" << query.lastError(); + return false; + } + emit powerBalanceEntryAdded(sampleRate, PowerBalanceLogEntry(timestamp, consumption, production, acquisition, storage)); + return true; +} + +bool EnergyLogger::sampleThingsPower(SampleRate sampleRate, SampleRate baseSampleRate, const QDateTime &sampleEnd) +{ + bool ret = true; + foreach (const ThingId &thingId, m_thingsPowerLiveLogs.keys()) { + ret &= sampleThingPower(thingId, sampleRate, baseSampleRate, sampleEnd); + } + return ret; +} + +bool EnergyLogger::sampleThingPower(const ThingId &thingId, SampleRate sampleRate, SampleRate baseSampleRate, const QDateTime &sampleEnd) +{ + QDateTime sampleStart = sampleEnd.addMSecs(-sampleRate * 60 * 1000); + + qCDebug(dcEnergyExperience()) << "Sampling" << sampleRate << "from" << sampleStart << "to" << sampleEnd; + + QSqlQuery query(m_db); + query.prepare("SELECT * FROM thingPower WHERE thingId = ? AND sampleRate = ? AND timestamp >= ? AND timestamp < ?;"); + query.addBindValue(thingId); + query.addBindValue(baseSampleRate); + query.addBindValue(sampleStart.toMSecsSinceEpoch()); + query.addBindValue(sampleEnd.toMSecsSinceEpoch()); + query.exec(); + + if (query.lastError().isValid()) { + qCWarning(dcEnergyExperience()) << "Error fetching thing power samples for" << baseSampleRate << "from" << sampleStart.toString() << "to" << sampleEnd.toString(); + qCWarning(dcEnergyExperience()) << "SQL error was:" << query.lastError() << "executed query:" << query.executedQuery(); + return false; + } + + double medianCurrentPower = 0; + while (query.next()) { + qCDebug(dcEnergyExperience()) << "Frame:" << query.value("currentPower").toDouble() << QDateTime::fromMSecsSinceEpoch(query.value("timestamp").toLongLong()).toString(); + medianCurrentPower += query.value("currentPower").toDouble(); + } + qCDebug(dcEnergyExperience()) << "Total:" << medianCurrentPower << "base samplerate" << baseSampleRate << "samplerate:" << sampleRate; + medianCurrentPower = medianCurrentPower * baseSampleRate / sampleRate; + + double totalConsumption = query.value("totalConsumption").toDouble(); + double totalProduction = query.value("totalProduction").toDouble(); + + qCDebug(dcEnergyExperience()) << "Sampled:" << medianCurrentPower; + return insertThingPower(sampleEnd, sampleRate, thingId, medianCurrentPower, totalConsumption, totalProduction); +} + +bool EnergyLogger::insertThingPower(const QDateTime ×tamp, SampleRate sampleRate, const ThingId &thingId, double currentPower, double totalConsumption, double totalProduction) +{ + QSqlQuery query = QSqlQuery(m_db); + query.prepare("INSERT INTO thingPower (timestamp, sampleRate, thingId, currentPower, totalConsumption, totalProduction) values (?, ?, ?, ?, ?, ?);"); + query.addBindValue(timestamp.toMSecsSinceEpoch()); + query.addBindValue(sampleRate); + query.addBindValue(thingId); + query.addBindValue(currentPower); + query.addBindValue(totalConsumption); + query.addBindValue(totalProduction); + query.exec(); + if (query.lastError().isValid()) { + qCWarning(dcEnergyExperience()) << "Error logging thing power sample:" << query.lastError() << query.executedQuery(); + return false; + } + emit thingPowerEntryAdded(sampleRate, ThingPowerLogEntry(timestamp, thingId, currentPower, totalConsumption, totalProduction)); + return true; +} + +void EnergyLogger::trimPowerBalance(SampleRate sampleRate, const QDateTime &beforeTime) +{ + QSqlQuery query(m_db); + query.prepare("DELETE FROM powerBalance WHERE sampleRate = ? AND timestamp < ?;"); + query.addBindValue(sampleRate); + query.addBindValue(beforeTime.toMSecsSinceEpoch()); + query.exec(); + if (query.numRowsAffected() > 0) { + qCDebug(dcEnergyExperience()).nospace() << "Trimmed " << query.numRowsAffected() << " from power balance series: " << sampleRate << " (Older than: " << beforeTime.toString() << ")"; + } +} + +void EnergyLogger::trimThingPower(const ThingId &thingId, SampleRate sampleRate, const QDateTime &beforeTime) +{ + QSqlQuery query(m_db); + query.prepare("DELETE FROM thingPower WHERE thingId = ? AND sampleRate = ? AND timestamp < ?;"); + query.addBindValue(thingId); + query.addBindValue(sampleRate); + query.addBindValue(beforeTime.toMSecsSinceEpoch()); + query.exec(); + if (query.numRowsAffected() > 0) { + qCDebug(dcEnergyExperience()).nospace() << "Trimmed " << query.numRowsAffected() << " from thing power series for: " << thingId << sampleRate << " (Older than: " << beforeTime.toString() << ")"; + } +} diff --git a/plugin/energylogger.h b/plugin/energylogger.h new file mode 100644 index 0000000..8832b15 --- /dev/null +++ b/plugin/energylogger.h @@ -0,0 +1,68 @@ +#ifndef ENERGYLOGGER_H +#define ENERGYLOGGER_H + +#include "energylogs.h" + +#include + +#include +#include +#include +#include +#include + +class EnergyLogger : public EnergyLogs +{ + Q_OBJECT +public: + explicit EnergyLogger(QObject *parent = nullptr); + + void logPowerBalance(double consumption, double production, double acquisition, double storage); + void logThingPower(const ThingId &thingId, double currentPower, double totalConsumption, double totalProduction); + + PowerBalanceLogEntries powerBalanceLogs(SampleRate sampleRate, const QDateTime &from = QDateTime(), const QDateTime &to = QDateTime()) const override; + ThingPowerLogEntries thingPowerLogs(SampleRate sampleRate, const QList &thingIds, const QDateTime &from = QDateTime(), const QDateTime &to = QDateTime()) const override; + +private slots: + void sample(); + +private: + bool initDB(); + void addConfig(SampleRate sampleRate, SampleRate baseSampleRate, int maxSamples); + QDateTime getOldestPowerBalanceSampleTimestamp(SampleRate sampleRate); + QDateTime getNewestPowerBalanceSampleTimestamp(SampleRate sampleRate); + QDateTime getOldestThingPowerSampleTimestamp(const ThingId &thingId, SampleRate sampleRate); + QDateTime getNewestThingPowerSampleTimestamp(const ThingId &thingId, SampleRate sampleRate); + + QDateTime nextSampleTimestamp(SampleRate sampleRate, const QDateTime &dateTime); + void scheduleNextSample(SampleRate sampleRate); + + void rectifySamples(SampleRate sampleRate, EnergyLogger::SampleRate baseSampleRate); + + bool samplePowerBalance(SampleRate sampleRate, SampleRate baseSampleRate, const QDateTime &sampleEnd); + bool insertPowerBalance(const QDateTime ×tamp, SampleRate sampleRate, double consumption, double production, double acquisition, double storage); + bool sampleThingsPower(SampleRate sampleRate, SampleRate baseSampleRate, const QDateTime &sampleEnd); + bool sampleThingPower(const ThingId &thingId, SampleRate sampleRate, SampleRate baseSampleRate, const QDateTime &sampleEnd); + bool insertThingPower(const QDateTime ×tamp, SampleRate sampleRate, const ThingId &thingId, double currentPower, double totalConsumption, double totalProduction); + void trimPowerBalance(SampleRate sampleRate, const QDateTime &beforeTime); + void trimThingPower(const ThingId &thingId, SampleRate sampleRate, const QDateTime &beforeTime); + +private: + struct SampleConfig { + SampleRate baseSampleRate; + int maxSamples = 0; + }; + + PowerBalanceLogEntries m_balanceLiveLog; + QHash m_thingsPowerLiveLogs; + + QTimer m_sampleTimer; + QHash m_nextSamples; + + QSqlDatabase m_db; + + int m_maxMinuteSamples = 0; + QMap m_configs; +}; + +#endif // ENERGYLOGGER_H diff --git a/plugin/energymanagerimpl.cpp b/plugin/energymanagerimpl.cpp index ac450fb..7b87807 100644 --- a/plugin/energymanagerimpl.cpp +++ b/plugin/energymanagerimpl.cpp @@ -1,5 +1,7 @@ #include "energymanagerimpl.h" -#include "nymeasettings.h" +#include "energylogger.h" + +#include #include @@ -7,7 +9,8 @@ Q_DECLARE_LOGGING_CATEGORY(dcEnergyExperience) EnergyManagerImpl::EnergyManagerImpl(ThingManager *thingManager, QObject *parent): EnergyManager(parent), - m_thingManager(thingManager) + m_thingManager(thingManager), + m_logger(new EnergyLogger(this)) { // Most of the time we get a bunch of signals at the same time (root meter, producers, consumers etc) // In order to decrease some load on the system, we'll wait for wee bit until we actually update to @@ -67,6 +70,16 @@ double EnergyManagerImpl::currentPowerAcquisition() const return m_currentPowerAcquisition; } +double EnergyManagerImpl::currentPowerStorage() const +{ + return m_currentPowerStorage; +} + +EnergyLogs *EnergyManagerImpl::logs() const +{ + return m_logger; +} + void EnergyManagerImpl::watchThing(Thing *thing) { // If we don't have a root meter yet, we'll be auto-setting the first energymeter that appears. @@ -76,8 +89,10 @@ void EnergyManagerImpl::watchThing(Thing *thing) } qCDebug(dcEnergyExperience()) << "Wathing thing:" << thing->name(); - if (thing->thingClass().interfaces().contains("smartmeterproducer") - || thing->thingClass().interfaces().contains("energymeter") + + // React on things that requie us updating the power balance + if (thing->thingClass().interfaces().contains("energymeter") + || thing->thingClass().interfaces().contains("smartmeterproducer") || thing->thingClass().interfaces().contains("energystorage")) { connect(thing, &Thing::stateValueChanged, this, [=](const StateTypeId &stateTypeId){ if (thing->thingClass().getStateType(stateTypeId).name() == "currentPower") { @@ -85,6 +100,18 @@ void EnergyManagerImpl::watchThing(Thing *thing) } }); } + + // React on things that need to be logged + if (thing->thingClass().interfaces().contains("energymeter") + || thing->thingClass().interfaces().contains("smartmeterconsumer") + || thing->thingClass().interfaces().contains("smartmeterproducer") + || thing->thingClass().interfaces().contains("energystorage")) { + connect(thing, &Thing::stateValueChanged, this, [=](const StateTypeId &stateTypeId, const QVariant &value){ + if (thing->thingClass().getStateType(stateTypeId).name() == "currentPower") { + m_logger->logThingPower(thing->id(), value.toDouble(), thing->state("totalEnergyConsumed").value().toDouble(), thing->state("totalEnergyProduced").value().toDouble()); + } + }); + } } void EnergyManagerImpl::unwatchThing(const ThingId &thingId) @@ -107,21 +134,25 @@ void EnergyManagerImpl::updatePowerBalance() currentPowerProduction += thing->stateValue("currentPower").toDouble(); } - double currentBatteryBalance = 0; + double currentPowerStorage = 0; foreach (Thing *thing, m_thingManager->configuredThings().filterByInterface("energystorage")) { - currentBatteryBalance += thing->stateValue("currentPower").toDouble(); + currentPowerStorage += thing->stateValue("currentPower").toDouble(); } - double currentPowerConsumption = -currentPowerProduction + currentPowerAcquisition - currentBatteryBalance; + double currentPowerConsumption = -currentPowerProduction + currentPowerAcquisition - currentPowerStorage; - qCDebug(dcEnergyExperience()) << "Consumption:" << currentPowerConsumption << "Production:" << currentPowerProduction << "Acquisition:" << currentPowerAcquisition << "Battery:" << currentBatteryBalance; + + qCDebug(dcEnergyExperience()) << "Consumption:" << currentPowerConsumption << "Production:" << currentPowerProduction << "Acquisition:" << currentPowerAcquisition << "Storage:" << currentPowerStorage; if (currentPowerAcquisition != m_currentPowerAcquisition || currentPowerConsumption != m_currentPowerConsumption - || currentPowerProduction != m_currentPowerProduction) { + || currentPowerProduction != m_currentPowerProduction + || currentPowerStorage != m_currentPowerStorage) { m_currentPowerAcquisition = currentPowerAcquisition; m_currentPowerProduction = currentPowerProduction; m_currentPowerConsumption = currentPowerConsumption; + m_currentPowerStorage = currentPowerStorage; emit powerBalanceChanged(); + m_logger->logPowerBalance(m_currentPowerConsumption, m_currentPowerProduction, m_currentPowerAcquisition, m_currentPowerStorage); } } diff --git a/plugin/energymanagerimpl.h b/plugin/energymanagerimpl.h index 1a70d90..617afa2 100644 --- a/plugin/energymanagerimpl.h +++ b/plugin/energymanagerimpl.h @@ -9,6 +9,8 @@ #include "energymanager.h" +class EnergyLogger; + class EnergyManagerImpl : public EnergyManager { Q_OBJECT @@ -21,6 +23,9 @@ public: double currentPowerConsumption() const override; double currentPowerProduction() const override; double currentPowerAcquisition() const override; + double currentPowerStorage() const override; + + EnergyLogs* logs() const override; private: void watchThing(Thing *thing); @@ -40,6 +45,9 @@ private: double m_currentPowerConsumption; double m_currentPowerProduction; double m_currentPowerAcquisition; + double m_currentPowerStorage; + + EnergyLogger *m_logger = nullptr; }; #endif // ENERGYMANAGERIMPL_H diff --git a/plugin/plugin.pro b/plugin/plugin.pro index 68acd49..8370364 100644 --- a/plugin/plugin.pro +++ b/plugin/plugin.pro @@ -5,7 +5,7 @@ CONFIG += plugin link_pkgconfig c++11 PKGCONFIG += nymea QT -= gui -QT += network +QT += network sql include(../config.pri) @@ -14,10 +14,12 @@ LIBS += -L$$top_builddir/libnymea-energy -lnymea-energy HEADERS += experiencepluginenergy.h \ energyjsonhandler.h \ + energylogger.h \ energymanagerimpl.h SOURCES += experiencepluginenergy.cpp \ energyjsonhandler.cpp \ + energylogger.cpp \ energymanagerimpl.cpp target.path = $$[QT_INSTALL_LIBS]/nymea/experiences/ From d684016498382b09f7835f8b5e2af54f8c265da0 Mon Sep 17 00:00:00 2001 From: Michael Zanetti Date: Thu, 18 Nov 2021 14:50:51 +0100 Subject: [PATCH 03/20] Add storage to power balance --- plugin/energyjsonhandler.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/plugin/energyjsonhandler.cpp b/plugin/energyjsonhandler.cpp index 22e402a..413ddb2 100644 --- a/plugin/energyjsonhandler.cpp +++ b/plugin/energyjsonhandler.cpp @@ -32,6 +32,7 @@ EnergyJsonHandler::EnergyJsonHandler(EnergyManager *energyManager, QObject *pare returns.insert("currentPowerConsumption", enumValueName(Double)); returns.insert("currentPowerProduction", enumValueName(Double)); returns.insert("currentPowerAcquisition", enumValueName(Double)); + returns.insert("currentPowerStorage", enumValueName(Double)); registerMethod("GetPowerBalance", description, params, returns); params.clear(); returns.clear(); @@ -65,6 +66,7 @@ EnergyJsonHandler::EnergyJsonHandler(EnergyManager *energyManager, QObject *pare params.insert("currentPowerConsumption", enumValueName(Double)); params.insert("currentPowerProduction", enumValueName(Double)); params.insert("currentPowerAcquisition", enumValueName(Double)); + params.insert("currentPowerStorage", enumValueName(Double)); registerNotification("PowerBalanceChanged", description, params); params.clear(); @@ -92,6 +94,7 @@ EnergyJsonHandler::EnergyJsonHandler(EnergyManager *energyManager, QObject *pare params.insert("currentPowerConsumption", m_energyManager->currentPowerConsumption()); params.insert("currentPowerProduction", m_energyManager->currentPowerProduction()); params.insert("currentPowerAcquisition", m_energyManager->currentPowerAcquisition()); + params.insert("currentPowerStorage", m_energyManager->currentPowerStorage()); emit PowerBalanceChanged(params); }); @@ -145,6 +148,7 @@ JsonReply *EnergyJsonHandler::GetPowerBalance(const QVariantMap ¶ms) ret.insert("currentPowerConsumption", m_energyManager->currentPowerConsumption()); ret.insert("currentPowerProduction", m_energyManager->currentPowerProduction()); ret.insert("currentPowerAcquisition", m_energyManager->currentPowerAcquisition()); + ret.insert("currentPowerStorage", m_energyManager->currentPowerStorage()); return createReply(ret); } From 034f90d7a984acf0a86d85b9747bd85c2496ccdb Mon Sep 17 00:00:00 2001 From: Michael Zanetti Date: Mon, 22 Nov 2021 00:43:36 +0100 Subject: [PATCH 04/20] Add totals to balance --- libnymea-energy/energylogs.cpp | 28 +++++++- libnymea-energy/energylogs.h | 16 ++++- libnymea-energy/energymanager.h | 4 ++ plugin/energyjsonhandler.cpp | 19 +++++- plugin/energylogger.cpp | 117 +++++++++++++++++++++++++------- plugin/energylogger.h | 14 +++- plugin/energymanagerimpl.cpp | 61 +++++++++++++++-- plugin/energymanagerimpl.h | 19 ++++-- 8 files changed, 239 insertions(+), 39 deletions(-) diff --git a/libnymea-energy/energylogs.cpp b/libnymea-energy/energylogs.cpp index 4483945..523078c 100644 --- a/libnymea-energy/energylogs.cpp +++ b/libnymea-energy/energylogs.cpp @@ -12,12 +12,16 @@ PowerBalanceLogEntry::PowerBalanceLogEntry() } -PowerBalanceLogEntry::PowerBalanceLogEntry(const QDateTime ×tamp, double consumption, double production, double acquisition, double storage): +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_storage(storage), + m_totalConsumption(totalConsumption), + m_totalProduction(totalProduction), + m_totalAcquisition(totalAcquisition), + m_totalReturn(totalReturn) { } @@ -47,6 +51,26 @@ 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)); diff --git a/libnymea-energy/energylogs.h b/libnymea-energy/energylogs.h index 9f92ae9..2881f2f 100644 --- a/libnymea-energy/energylogs.h +++ b/libnymea-energy/energylogs.h @@ -20,6 +20,7 @@ public: virtual ~EnergyLogs() = default; enum SampleRate { + SampleRateAny = 0, SampleRate1Min = 1, SampleRate15Mins = 15, SampleRate1Hour = 60, @@ -57,20 +58,33 @@ class PowerBalanceLogEntry 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); + 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) diff --git a/libnymea-energy/energymanager.h b/libnymea-energy/energymanager.h index 8a560cd..2d18f5e 100644 --- a/libnymea-energy/energymanager.h +++ b/libnymea-energy/energymanager.h @@ -61,6 +61,10 @@ public: 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; diff --git a/plugin/energyjsonhandler.cpp b/plugin/energyjsonhandler.cpp index 413ddb2..954e83b 100644 --- a/plugin/energyjsonhandler.cpp +++ b/plugin/energyjsonhandler.cpp @@ -33,6 +33,10 @@ EnergyJsonHandler::EnergyJsonHandler(EnergyManager *energyManager, QObject *pare 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(); @@ -67,6 +71,10 @@ EnergyJsonHandler::EnergyJsonHandler(EnergyManager *energyManager, QObject *pare 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(); @@ -95,6 +103,10 @@ EnergyJsonHandler::EnergyJsonHandler(EnergyManager *energyManager, QObject *pare 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); }); @@ -149,16 +161,17 @@ JsonReply *EnergyJsonHandler::GetPowerBalance(const QVariantMap ¶ms) 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) { - qCDebug(dcEnergyExperience()) << "params" << params; - qCDebug(dcEnergyExperience()) << "from" << params.value("from"); EnergyLogs::SampleRate sampleRate = enumNameToValue(params.value("sampleRate").toString()); QDateTime from = params.contains("from") ? QDateTime::fromMSecsSinceEpoch(params.value("from").toLongLong() * 1000) : QDateTime(); - qCDebug(dcEnergyExperience()) << "from2" << from; QDateTime to = params.contains("to") ? QDateTime::fromMSecsSinceEpoch(params.value("to").toLongLong() * 1000) : QDateTime(); QVariantMap returns; returns.insert("powerBalanceLogEntries", pack(m_energyManager->logs()->powerBalanceLogs(sampleRate, from, to))); diff --git a/plugin/energylogger.cpp b/plugin/energylogger.cpp index 6ff2ef2..876ac8e 100644 --- a/plugin/energylogger.cpp +++ b/plugin/energylogger.cpp @@ -6,6 +6,7 @@ #include #include #include +#include #include #include @@ -53,7 +54,6 @@ EnergyLogger::EnergyLogger(QObject *parent) : EnergyLogs(parent) } // Start the scheduling - scheduleNextSample(SampleRate1Min); foreach (SampleRate sampleRate, m_configs.keys()) { scheduleNextSample(sampleRate); @@ -71,9 +71,9 @@ EnergyLogger::EnergyLogger(QObject *parent) : EnergyLogs(parent) m_sampleTimer.start(1000); } -void EnergyLogger::logPowerBalance(double consumption, double production, double acquisition, double storage) +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); + 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); @@ -124,8 +124,8 @@ PowerBalanceLogEntries EnergyLogger::powerBalanceLogs(SampleRate sampleRate, con } while (query.next()) { - qCDebug(dcEnergyExperience()) << "Adding result"; - result.append(PowerBalanceLogEntry(QDateTime::fromMSecsSinceEpoch(query.value("timestamp").toLongLong()), query.value("consumption").toDouble(), query.value("production").toDouble(), query.value("acquisition").toDouble(), query.value("storage").toDouble())); +// qCDebug(dcEnergyExperience()) << "Adding result"; + result.append(queryResultToBalanceLogEntry(query.record())); } return result; } @@ -178,6 +178,33 @@ ThingPowerLogEntries EnergyLogger::thingPowerLogs(SampleRate sampleRate, const Q } +PowerBalanceLogEntry EnergyLogger::latestLogEntry(SampleRate sampleRate) +{ + 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(); + } + qCDebug(dcEnergyExperience()) << "Loaded latest log entry:" << query.record(); + return queryResultToBalanceLogEntry(query.record()); +} + void EnergyLogger::sample() { QDateTime now = QDateTime::currentDateTime(); @@ -210,8 +237,19 @@ void EnergyLogger::sample() 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()) << "Power balance for sample:" << medianConsumption << medianProduction << medianAcquisition << medianStorage << "duration:" << sampleStart.msecsTo(sampleEnd); - insertPowerBalance(sampleEnd, SampleRate1Min, medianConsumption, medianProduction, medianAcquisition, medianStorage); + insertPowerBalance(sampleEnd, SampleRate1Min, medianConsumption, medianProduction, medianAcquisition, medianStorage, totalConsumption, totalProduction, totalAcquisition, totalReturn); + m_lastSampleTotalConsumption = totalConsumption; + m_lastSampleTotalProducation = totalProduction; + m_lastSampleTotalAcquisition = totalAcquisition; + m_lastSampleTotalReturn = totalReturn; foreach (const ThingId &thingId, m_thingsPowerLiveLogs.keys()) { medianConsumption = 0; @@ -307,7 +345,11 @@ bool EnergyLogger::initDB() "consumption FLOAT," "production FLOAT," "acquisition FLOAT," - "storage FLOAT" + "storage FLOAT," + "totalConsumption FLOAT," + "totalProduction FLOAT," + "totalAcquisition FLOAT," + "totalReturn FLOAT" ");"); if (m_db.lastError().isValid()) { @@ -333,7 +375,7 @@ bool EnergyLogger::initDB() } } - qCDebug(dcEnergyExperience()) << "Initialized logging DB successfully."; + qCDebug(dcEnergyExperience()) << "Initialized logging DB successfully." << m_db.databaseName(); return true; } @@ -412,16 +454,16 @@ void EnergyLogger::rectifySamples(SampleRate sampleRate, SampleRate baseSampleRa QDateTime newestSample = getNewestPowerBalanceSampleTimestamp(sampleRate); qCDebug(dcEnergyExperience()) << "Checking for missing samples for" << sampleRate; - qCDebug(dcEnergyExperience()) << "Newest sample:" << newestSample.toString() << "Oldest base sample:" << oldestBaseSample.toString(); +// 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."; +// 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(); +// 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(); +// qCDebug(dcEnergyExperience()) << "Rectifying missed sample for" << sampleRate << "from" << nextSample.toString(); samplePowerBalance(sampleRate, baseSampleRate, nextSample); newestSample = nextSample; } @@ -430,17 +472,17 @@ void EnergyLogger::rectifySamples(SampleRate sampleRate, SampleRate baseSampleRa 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(); +// 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."; +// 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(); +// 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(); +// qCDebug(dcEnergyExperience()) << "T Rectifying missed sample for" << sampleRate << "from" << nextSample.toString(); sampleThingPower(thingId, sampleRate, baseSampleRate, nextSample); newestSample = nextSample; } @@ -453,6 +495,9 @@ QDateTime EnergyLogger::nextSampleTimestamp(SampleRate sampleRate, const QDateTi 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); @@ -512,12 +557,20 @@ bool EnergyLogger::samplePowerBalance(SampleRate sampleRate, SampleRate baseSamp double medianProduction = 0; double medianAcquisition = 0; double medianStorage = 0; + double totalConsumption = 0; + double totalProduction = 0; + double totalAcquisition = 0; + double totalReturn = 0; while (query.next()) { qCDebug(dcEnergyExperience()) << "Frame:" << query.value("consumption").toDouble() << query.value("production").toDouble() << query.value("acquisition").toDouble() << QDateTime::fromMSecsSinceEpoch(query.value("timestamp").toLongLong()).toString(); medianConsumption += query.value("consumption").toDouble(); medianProduction += query.value("production").toDouble(); medianAcquisition += query.value("acquisition").toDouble(); medianStorage += query.value("storage").toDouble(); + totalConsumption = query.value("totalConsumption").toDouble(); + totalProduction = query.value("totalProduction").toDouble(); + totalAcquisition = query.value("totalAcquisition").toDouble(); + totalReturn = query.value("totalReturn").toDouble(); } qCDebug(dcEnergyExperience()) << "Totals:" << medianConsumption << medianProduction << medianAcquisition << medianStorage << "base samplerate" << baseSampleRate << "samplerate:" << sampleRate; medianConsumption = medianConsumption * baseSampleRate / sampleRate; @@ -526,25 +579,29 @@ bool EnergyLogger::samplePowerBalance(SampleRate sampleRate, SampleRate baseSamp medianStorage = medianStorage * baseSampleRate / sampleRate; qCDebug(dcEnergyExperience()) << "Sampled:" << medianConsumption << medianProduction << medianAcquisition << medianStorage; - return insertPowerBalance(sampleEnd, sampleRate, medianConsumption, medianProduction, medianAcquisition, medianStorage); + 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) +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) values (?, ?, ?, ?, ?, ?);"); + 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(); return false; } - emit powerBalanceEntryAdded(sampleRate, PowerBalanceLogEntry(timestamp, consumption, production, acquisition, storage)); + emit powerBalanceEntryAdded(sampleRate, PowerBalanceLogEntry(timestamp, consumption, production, acquisition, storage, totalConsumption, totalProduction, totalAcquisition, totalReturn)); return true; } @@ -635,3 +692,17 @@ void EnergyLogger::trimThingPower(const ThingId &thingId, SampleRate sampleRate, 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()); + +} diff --git a/plugin/energylogger.h b/plugin/energylogger.h index 8832b15..fed393f 100644 --- a/plugin/energylogger.h +++ b/plugin/energylogger.h @@ -8,6 +8,7 @@ #include #include #include +#include #include #include @@ -17,12 +18,14 @@ class EnergyLogger : public EnergyLogs public: explicit EnergyLogger(QObject *parent = nullptr); - void logPowerBalance(double consumption, double production, double acquisition, double storage); + 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); + private slots: void sample(); @@ -40,13 +43,15 @@ private: void rectifySamples(SampleRate sampleRate, EnergyLogger::SampleRate baseSampleRate); bool samplePowerBalance(SampleRate sampleRate, SampleRate baseSampleRate, const QDateTime &sampleEnd); - bool insertPowerBalance(const QDateTime ×tamp, SampleRate sampleRate, double consumption, double production, double acquisition, double storage); + bool 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; + private: struct SampleConfig { SampleRate baseSampleRate; @@ -59,6 +64,11 @@ private: QTimer m_sampleTimer; QHash m_nextSamples; + double m_lastSampleTotalConsumption = 0; + double m_lastSampleTotalProducation = 0; + double m_lastSampleTotalAcquisition = 0; + double m_lastSampleTotalReturn = 0; + QSqlDatabase m_db; int m_maxMinuteSamples = 0; diff --git a/plugin/energymanagerimpl.cpp b/plugin/energymanagerimpl.cpp index 7b87807..3444ea7 100644 --- a/plugin/energymanagerimpl.cpp +++ b/plugin/energymanagerimpl.cpp @@ -24,6 +24,13 @@ EnergyManagerImpl::EnergyManagerImpl(ThingManager *thingManager, QObject *parent 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()) << "Loader power balance totals. Consumption:" << m_totalConsumption << "Production:" << m_totalProduction << "Acquisition:" << m_totalAcquisition << "Return:" << m_totalReturn; + foreach (Thing *thing, m_thingManager->configuredThings()) { watchThing(thing); } @@ -75,6 +82,26 @@ 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; @@ -106,6 +133,10 @@ void EnergyManagerImpl::watchThing(Thing *thing) || thing->thingClass().interfaces().contains("smartmeterconsumer") || thing->thingClass().interfaces().contains("smartmeterproducer") || thing->thingClass().interfaces().contains("energystorage")) { + + m_totalEnergyConsumedCache[thing] = thing->stateValue("totalEnergyConsumed").toDouble(); + m_totalEnergyProducedCache[thing] = thing->stateValue("totalEnergyProduced").toDouble(); + connect(thing, &Thing::stateValueChanged, this, [=](const StateTypeId &stateTypeId, const QVariant &value){ if (thing->thingClass().getStateType(stateTypeId).name() == "currentPower") { m_logger->logThingPower(thing->id(), value.toDouble(), thing->state("totalEnergyConsumed").value().toDouble(), thing->state("totalEnergyProduced").value().toDouble()); @@ -127,22 +158,44 @@ void EnergyManagerImpl::updatePowerBalance() double currentPowerAcquisition = 0; if (m_rootMeter) { currentPowerAcquisition = m_rootMeter->stateValue("currentPower").toDouble(); + + double oldAcquisition = m_totalEnergyConsumedCache.value(m_rootMeter); + double newAcquisition = m_rootMeter->stateValue("totalEnergyConsumed").toDouble(); + qCDebug(dcEnergyExperience()) << "Root meteter total consumption diff" << "old" << oldAcquisition << " new" << newAcquisition << (newAcquisition -oldAcquisition); + m_totalAcquisition += newAcquisition - oldAcquisition; + m_totalEnergyConsumedCache[m_rootMeter] = newAcquisition; + + double oldReturn = m_totalEnergyProducedCache.value(m_rootMeter); + double newReturn = m_rootMeter->stateValue("totalEnergyProduced").toDouble(); + qCDebug(dcEnergyExperience()) << "Root meteter total return diff" << "old" << oldReturn << " new" << newReturn << (newReturn - oldReturn); + m_totalReturn += newReturn - oldReturn; + m_totalEnergyProducedCache[m_rootMeter] = newReturn; } double currentPowerProduction = 0; foreach (Thing* thing, m_thingManager->configuredThings().filterByInterface("smartmeterproducer")) { currentPowerProduction += thing->stateValue("currentPower").toDouble(); + double oldProduction = m_totalEnergyProducedCache.value(thing); + double newProduction = thing->stateValue("totalEnergyProduced").toDouble(); + qCDebug(dcEnergyExperience()) << "inverter total production diff" << "old" << oldProduction << " new" << newProduction << (newProduction - oldProduction); + m_totalProduction += newProduction - oldProduction; + m_totalEnergyProducedCache[thing] = newProduction; } double currentPowerStorage = 0; + double totalFromStorage = 0; foreach (Thing *thing, m_thingManager->configuredThings().filterByInterface("energystorage")) { currentPowerStorage += thing->stateValue("currentPower").toDouble(); + double oldProduction = m_totalEnergyProducedCache.value(thing); + double newProduction = thing->stateValue("totalEnergyProduced").toDouble(); + totalFromStorage += newProduction - oldProduction; + m_totalEnergyProducedCache[thing] = newProduction; } - double currentPowerConsumption = -currentPowerProduction + currentPowerAcquisition - currentPowerStorage; + double currentPowerConsumption = currentPowerAcquisition + qAbs(qMin(0.0, currentPowerProduction)) - currentPowerStorage; + m_totalConsumption = m_totalAcquisition + m_totalProduction + totalFromStorage; - - qCDebug(dcEnergyExperience()) << "Consumption:" << currentPowerConsumption << "Production:" << currentPowerProduction << "Acquisition:" << currentPowerAcquisition << "Storage:" << currentPowerStorage; + 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 @@ -152,7 +205,7 @@ void EnergyManagerImpl::updatePowerBalance() m_currentPowerConsumption = currentPowerConsumption; m_currentPowerStorage = currentPowerStorage; emit powerBalanceChanged(); - m_logger->logPowerBalance(m_currentPowerConsumption, m_currentPowerProduction, m_currentPowerAcquisition, m_currentPowerStorage); + m_logger->logPowerBalance(m_currentPowerConsumption, m_currentPowerProduction, m_currentPowerAcquisition, m_currentPowerStorage, m_totalConsumption, m_totalProduction, m_totalAcquisition, m_totalReturn); } } diff --git a/plugin/energymanagerimpl.h b/plugin/energymanagerimpl.h index 617afa2..6b172a9 100644 --- a/plugin/energymanagerimpl.h +++ b/plugin/energymanagerimpl.h @@ -24,6 +24,10 @@ public: 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; @@ -42,12 +46,19 @@ private: Thing *m_rootMeter = nullptr; QTimer m_balanceUpdateTimer; - double m_currentPowerConsumption; - double m_currentPowerProduction; - double m_currentPowerAcquisition; - double m_currentPowerStorage; + 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; + + QHash m_totalEnergyConsumedCache; + QHash m_totalEnergyProducedCache; }; #endif // ENERGYMANAGERIMPL_H From a34dd7642bbb00709d034ea98b92b1f45bbc23af Mon Sep 17 00:00:00 2001 From: Michael Zanetti Date: Tue, 23 Nov 2021 00:47:11 +0100 Subject: [PATCH 05/20] Fixes --- libnymea-energy/energylogs.h | 2 +- plugin/energyjsonhandler.cpp | 2 +- plugin/energylogger.cpp | 6 ++++-- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/libnymea-energy/energylogs.h b/libnymea-energy/energylogs.h index 2881f2f..79c89a2 100644 --- a/libnymea-energy/energylogs.h +++ b/libnymea-energy/energylogs.h @@ -102,7 +102,7 @@ Q_DECLARE_METATYPE(PowerBalanceLogEntries) class ThingPowerLogEntry { Q_GADGET - Q_PROPERTY(QDateTime timstamp READ timestamp) + Q_PROPERTY(QDateTime timestamp READ timestamp) Q_PROPERTY(QUuid thingId READ thingId) Q_PROPERTY(double currentPower READ currentPower) Q_PROPERTY(double totalConsumption READ totalConsumption) diff --git a/plugin/energyjsonhandler.cpp b/plugin/energyjsonhandler.cpp index 954e83b..052266e 100644 --- a/plugin/energyjsonhandler.cpp +++ b/plugin/energyjsonhandler.cpp @@ -188,6 +188,6 @@ JsonReply *EnergyJsonHandler::GetThingPowerLogs(const QVariantMap ¶ms) 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))); + returns.insert("thingPowerLogEntries", pack(m_energyManager->logs()->thingPowerLogs(sampleRate, thingIds, from, to))); return createReply(returns); } diff --git a/plugin/energylogger.cpp b/plugin/energylogger.cpp index 876ac8e..a223b8a 100644 --- a/plugin/energylogger.cpp +++ b/plugin/energylogger.cpp @@ -139,13 +139,15 @@ ThingPowerLogEntries EnergyLogger::thingPowerLogs(SampleRate sampleRate, const Q 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 ") + " )"; + queryString += " AND (" + thingsQuery.join(" OR ") + " )"; } if (!from.isNull()) { @@ -598,7 +600,7 @@ bool EnergyLogger::insertPowerBalance(const QDateTime ×tamp, SampleRate sam query.addBindValue(totalReturn); query.exec(); if (query.lastError().isValid()) { - qCWarning(dcEnergyExperience()) << "Error logging consumption sample:" << query.lastError(); + 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)); From 602347dba1283fa0e0c416bee59e1a3b245bd8f4 Mon Sep 17 00:00:00 2001 From: Michael Zanetti Date: Tue, 23 Nov 2021 14:46:22 +0100 Subject: [PATCH 06/20] Housekeeping on energy logs (removal of removed things) --- plugin/energylogger.cpp | 39 +++++++++++++++++++++++++++--------- plugin/energylogger.h | 3 +++ plugin/energymanagerimpl.cpp | 10 +++++++++ 3 files changed, 43 insertions(+), 9 deletions(-) diff --git a/plugin/energylogger.cpp b/plugin/energylogger.cpp index a223b8a..f77c84f 100644 --- a/plugin/energylogger.cpp +++ b/plugin/energylogger.cpp @@ -42,15 +42,8 @@ EnergyLogger::EnergyLogger(QObject *parent) : EnergyLogs(parent) addConfig(SampleRate1Year, SampleRate1Month, 20); // 20 years // Load thingIds from logs so we have the complete list available for sampling, even if a thing might not produce any logs for a while. - QSqlQuery query(m_db); - query.prepare("SELECT DISTINCT thingId FROM thingPower;"); - query.exec(); - if (query.lastError().isValid()) { - qCWarning(dcEnergyExperience()) << "Failed to load existing things from logs:" << query.lastError(); - } else { - while (query.next()) { - m_thingsPowerLiveLogs[query.value("thingId").toUuid()] = ThingPowerLogEntries(); - } + foreach (const ThingId &thingId, loggedThings()) { + m_thingsPowerLiveLogs[thingId] = ThingPowerLogEntries(); } // Start the scheduling @@ -207,6 +200,34 @@ PowerBalanceLogEntry EnergyLogger::latestLogEntry(SampleRate sampleRate) return queryResultToBalanceLogEntry(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::sample() { QDateTime now = QDateTime::currentDateTime(); diff --git a/plugin/energylogger.h b/plugin/energylogger.h index fed393f..dcf7a3c 100644 --- a/plugin/energylogger.h +++ b/plugin/energylogger.h @@ -26,6 +26,9 @@ public: PowerBalanceLogEntry latestLogEntry(SampleRate sampleRate); + void removeThingLogs(const ThingId &thingId); + QList loggedThings() const; + private slots: void sample(); diff --git a/plugin/energymanagerimpl.cpp b/plugin/energymanagerimpl.cpp index 3444ea7..d361f20 100644 --- a/plugin/energymanagerimpl.cpp +++ b/plugin/energymanagerimpl.cpp @@ -36,6 +36,14 @@ EnergyManagerImpl::EnergyManagerImpl(ThingManager *thingManager, QObject *parent } 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 @@ -151,6 +159,8 @@ void EnergyManagerImpl::unwatchThing(const ThingId &thingId) m_rootMeter = nullptr; emit rootMeterChanged(); } + + m_logger->removeThingLogs(thingId); } void EnergyManagerImpl::updatePowerBalance() From 10d323a3aa17a8d7f8072156844228caac49abb5 Mon Sep 17 00:00:00 2001 From: Michael Zanetti Date: Tue, 23 Nov 2021 16:43:43 +0100 Subject: [PATCH 07/20] Fix sampleing of things totalConsumption/totalProduction --- plugin/energylogger.cpp | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/plugin/energylogger.cpp b/plugin/energylogger.cpp index f77c84f..50fe593 100644 --- a/plugin/energylogger.cpp +++ b/plugin/energylogger.cpp @@ -658,17 +658,17 @@ bool EnergyLogger::sampleThingPower(const ThingId &thingId, SampleRate sampleRat } double medianCurrentPower = 0; + double totalConsumption = 0; + double totalProduction = 0; while (query.next()) { 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(); } - qCDebug(dcEnergyExperience()) << "Total:" << medianCurrentPower << "base samplerate" << baseSampleRate << "samplerate:" << sampleRate; medianCurrentPower = medianCurrentPower * baseSampleRate / sampleRate; - double totalConsumption = query.value("totalConsumption").toDouble(); - double totalProduction = query.value("totalProduction").toDouble(); - - qCDebug(dcEnergyExperience()) << "Sampled:" << medianCurrentPower; + qCDebug(dcEnergyExperience()) << "Sampled:" << thingId << sampleRate << "media currentpower:" << medianCurrentPower << "total consumption:" << totalConsumption << "total production:" << totalProduction; return insertThingPower(sampleEnd, sampleRate, thingId, medianCurrentPower, totalConsumption, totalProduction); } From 6e312fe02ce9f0b5bb8abe7c4740332f81209a6b Mon Sep 17 00:00:00 2001 From: Michael Zanetti Date: Thu, 25 Nov 2021 00:43:33 +0100 Subject: [PATCH 08/20] Fix trimming of logs --- plugin/energylogger.cpp | 4 ++-- plugin/energymanagerimpl.cpp | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/plugin/energylogger.cpp b/plugin/energylogger.cpp index 50fe593..3087a3d 100644 --- a/plugin/energylogger.cpp +++ b/plugin/energylogger.cpp @@ -313,7 +313,7 @@ void EnergyLogger::sample() // and then trim them if (now > m_nextSamples.value(SampleRate1Min)) { QDateTime sampleTime = m_nextSamples.value(SampleRate1Min); - QDateTime oldestTimestamp = sampleTime.addMSecs(-m_maxMinuteSamples * 60 * 1000); + QDateTime oldestTimestamp = sampleTime.addMSecs(-(qulonglong)m_maxMinuteSamples * 60 * 1000); trimPowerBalance(SampleRate1Min, oldestTimestamp); foreach (const ThingId &thingId, m_thingsPowerLiveLogs.keys()) { trimThingPower(thingId, SampleRate1Min, oldestTimestamp); @@ -322,7 +322,7 @@ void EnergyLogger::sample() foreach (SampleRate sampleRate, m_configs.keys()) { if (now >= m_nextSamples.value(sampleRate)) { QDateTime sampleTime = m_nextSamples.value(sampleRate); - QDateTime oldestTimestamp = sampleTime.addMSecs(-m_configs.value(sampleRate).maxSamples * sampleRate * 60 * 1000); + QDateTime oldestTimestamp = sampleTime.addMSecs(-(qulonglong)m_configs.value(sampleRate).maxSamples * sampleRate * 60 * 1000); trimPowerBalance(sampleRate, oldestTimestamp); foreach (const ThingId &thingId, m_thingsPowerLiveLogs.keys()) { trimThingPower(thingId, sampleRate, oldestTimestamp); diff --git a/plugin/energymanagerimpl.cpp b/plugin/energymanagerimpl.cpp index d361f20..ea3eef5 100644 --- a/plugin/energymanagerimpl.cpp +++ b/plugin/energymanagerimpl.cpp @@ -29,7 +29,7 @@ EnergyManagerImpl::EnergyManagerImpl(ThingManager *thingManager, QObject *parent m_totalProduction = latestEntry.totalProduction(); m_totalAcquisition = latestEntry.totalAcquisition(); m_totalReturn = latestEntry.totalReturn(); - qCDebug(dcEnergyExperience()) << "Loader power balance totals. Consumption:" << m_totalConsumption << "Production:" << m_totalProduction << "Acquisition:" << m_totalAcquisition << "Return:" << m_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); @@ -171,7 +171,7 @@ void EnergyManagerImpl::updatePowerBalance() double oldAcquisition = m_totalEnergyConsumedCache.value(m_rootMeter); double newAcquisition = m_rootMeter->stateValue("totalEnergyConsumed").toDouble(); - qCDebug(dcEnergyExperience()) << "Root meteter total consumption diff" << "old" << oldAcquisition << " new" << newAcquisition << (newAcquisition -oldAcquisition); + qCDebug(dcEnergyExperience()) << "Root meteter total consumption: Previous value:" << oldAcquisition << "New value:" << newAcquisition << "Diff:" << (newAcquisition -oldAcquisition); m_totalAcquisition += newAcquisition - oldAcquisition; m_totalEnergyConsumedCache[m_rootMeter] = newAcquisition; From f63e9bcaf0c3deb4b31992fb22ff872e3551a1aa Mon Sep 17 00:00:00 2001 From: Michael Zanetti Date: Fri, 26 Nov 2021 15:11:54 +0100 Subject: [PATCH 09/20] More logs --- plugin/energylogger.cpp | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/plugin/energylogger.cpp b/plugin/energylogger.cpp index 3087a3d..ccf39f2 100644 --- a/plugin/energylogger.cpp +++ b/plugin/energylogger.cpp @@ -109,7 +109,7 @@ PowerBalanceLogEntries EnergyLogger::powerBalanceLogs(SampleRate sampleRate, con query.addBindValue(bindValue); } - qCDebug(dcEnergyExperience()) << "Executing" << queryString << query.executedQuery() << bindValues; + qCDebug(dcEnergyExperience()) << "Executing" << queryString << bindValues; query.exec(); if (query.lastError().isValid()) { qCWarning(dcEnergyExperience()) << "Error fetching power balance logs:" << query.lastError() << query.executedQuery(); @@ -236,7 +236,7 @@ void EnergyLogger::sample() QDateTime sampleEnd = m_nextSamples.value(SampleRate1Min); QDateTime sampleStart = sampleEnd.addMSecs(-60 * 1000); - qCDebug(dcEnergyExperience()) << "Sampling 1 min" << sampleEnd.toString(); + qCDebug(dcEnergyExperience()) << "Sampling power balance for 1 min" << sampleEnd.toString(); double medianConsumption = 0; double medianProduction = 0; @@ -267,7 +267,7 @@ void EnergyLogger::sample() double totalAcquisition = newest.totalAcquisition(); double totalReturn = newest.totalReturn(); - qCDebug(dcEnergyExperience()) << "Power balance for sample:" << medianConsumption << medianProduction << medianAcquisition << medianStorage << "duration:" << sampleStart.msecsTo(sampleEnd); + qCDebug(dcEnergyExperience()) << "Sampled:" << "🔥:" << medianConsumption << "🌞:" << medianProduction << "💵:" << medianAcquisition << "🔋:" << medianStorage << "Totals:" << "🔥:" << totalConsumption << "🌞:" << totalProduction << "💵↓:" << totalAcquisition << "💵↑:" << totalReturn; insertPowerBalance(sampleEnd, SampleRate1Min, medianConsumption, medianProduction, medianAcquisition, medianStorage, totalConsumption, totalProduction, totalAcquisition, totalReturn); m_lastSampleTotalConsumption = totalConsumption; m_lastSampleTotalProducation = totalProduction; @@ -276,6 +276,7 @@ void EnergyLogger::sample() foreach (const ThingId &thingId, m_thingsPowerLiveLogs.keys()) { medianConsumption = 0; + qCDebug(dcEnergyExperience()) << "Sampling thing energy for 1 min:" << thingId; ThingPowerLogEntries entries = m_thingsPowerLiveLogs.value(thingId); for (int i = 0; i < entries.count(); i++) { const ThingPowerLogEntry &entry = entries.at(i); @@ -295,7 +296,7 @@ void EnergyLogger::sample() totalConsumption = entries.last().totalConsumption(); totalProduction = entries.last().totalProduction(); } - qCDebug(dcEnergyExperience()) << "Thing power of sample:" << medianConsumption << totalConsumption << totalProduction << "total duration:" << sampleStart.msecsTo(sampleEnd); + qCDebug(dcEnergyExperience()) << "Sampled:" << "🔥:" << medianConsumption << "🌞:" << medianProduction << "Totals:" << "🔥:" << totalConsumption << "🌞:" << totalProduction; insertThingPower(sampleEnd, SampleRate1Min, thingId, medianConsumption, totalConsumption, totalProduction); } } @@ -313,7 +314,7 @@ void EnergyLogger::sample() // and then trim them if (now > m_nextSamples.value(SampleRate1Min)) { QDateTime sampleTime = m_nextSamples.value(SampleRate1Min); - QDateTime oldestTimestamp = sampleTime.addMSecs(-(qulonglong)m_maxMinuteSamples * 60 * 1000); + QDateTime oldestTimestamp = sampleTime.addMSecs(-(qint64)m_maxMinuteSamples * 60 * 1000); trimPowerBalance(SampleRate1Min, oldestTimestamp); foreach (const ThingId &thingId, m_thingsPowerLiveLogs.keys()) { trimThingPower(thingId, SampleRate1Min, oldestTimestamp); @@ -322,7 +323,7 @@ void EnergyLogger::sample() foreach (SampleRate sampleRate, m_configs.keys()) { if (now >= m_nextSamples.value(sampleRate)) { QDateTime sampleTime = m_nextSamples.value(sampleRate); - QDateTime oldestTimestamp = sampleTime.addMSecs(-(qulonglong)m_configs.value(sampleRate).maxSamples * sampleRate * 60 * 1000); + 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); @@ -585,7 +586,7 @@ bool EnergyLogger::samplePowerBalance(SampleRate sampleRate, SampleRate baseSamp double totalAcquisition = 0; double totalReturn = 0; while (query.next()) { - qCDebug(dcEnergyExperience()) << "Frame:" << query.value("consumption").toDouble() << query.value("production").toDouble() << query.value("acquisition").toDouble() << QDateTime::fromMSecsSinceEpoch(query.value("timestamp").toLongLong()).toString(); + 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(); @@ -595,13 +596,12 @@ bool EnergyLogger::samplePowerBalance(SampleRate sampleRate, SampleRate baseSamp totalAcquisition = query.value("totalAcquisition").toDouble(); totalReturn = query.value("totalReturn").toDouble(); } - qCDebug(dcEnergyExperience()) << "Totals:" << medianConsumption << medianProduction << medianAcquisition << medianStorage << "base samplerate" << baseSampleRate << "samplerate:" << sampleRate; medianConsumption = medianConsumption * baseSampleRate / sampleRate; medianProduction = medianProduction * baseSampleRate / sampleRate; medianAcquisition = medianAcquisition * baseSampleRate / sampleRate; medianStorage = medianStorage * baseSampleRate / sampleRate; - qCDebug(dcEnergyExperience()) << "Sampled:" << medianConsumption << medianProduction << medianAcquisition << medianStorage; + qCDebug(dcEnergyExperience()) << "Sampled:" << "🔥:" << medianConsumption << "🌞:" << medianProduction << "💵:" << medianAcquisition << "🔋:" << medianStorage << "Totals:" << "🔥:" << totalConsumption << "🌞:" << totalProduction << "💵↓:" << totalAcquisition << "💵↑:" << totalReturn; return insertPowerBalance(sampleEnd, sampleRate, medianConsumption, medianProduction, medianAcquisition, medianStorage, totalConsumption, totalProduction, totalAcquisition, totalReturn); } From fd1fd490df04a68cfbeed997477d3c4854a9c0be Mon Sep 17 00:00:00 2001 From: Michael Zanetti Date: Fri, 26 Nov 2021 16:43:04 +0100 Subject: [PATCH 10/20] Fix loading of previous values at startup --- plugin/energylogger.cpp | 29 ++++++++++++++++++++++++++++- plugin/energylogger.h | 2 ++ plugin/energymanagerimpl.cpp | 13 ++++++++----- 3 files changed, 38 insertions(+), 6 deletions(-) diff --git a/plugin/energylogger.cpp b/plugin/energylogger.cpp index ccf39f2..3b70433 100644 --- a/plugin/energylogger.cpp +++ b/plugin/energylogger.cpp @@ -87,6 +87,7 @@ void EnergyLogger::logThingPower(const ThingId &thingId, double currentPower, do m_thingsPowerLiveLogs[thingId].removeLast(); } } + PowerBalanceLogEntries EnergyLogger::powerBalanceLogs(SampleRate sampleRate, const QDateTime &from, const QDateTime &to) const { PowerBalanceLogEntries result; @@ -196,10 +197,27 @@ PowerBalanceLogEntry EnergyLogger::latestLogEntry(SampleRate sampleRate) qCDebug(dcEnergyExperience()) << "No power balance log entry in DB for sample rate:" << sampleRate; return PowerBalanceLogEntry(); } - qCDebug(dcEnergyExperience()) << "Loaded latest log entry:" << query.record(); return queryResultToBalanceLogEntry(query.record()); } +ThingPowerLogEntry EnergyLogger::latestLogEntry(SampleRate sampleRate, const ThingId &thingId) +{ + 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); @@ -729,3 +747,12 @@ PowerBalanceLogEntry EnergyLogger::queryResultToBalanceLogEntry(const QSqlRecord 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 index dcf7a3c..f90ed18 100644 --- a/plugin/energylogger.h +++ b/plugin/energylogger.h @@ -25,6 +25,7 @@ public: 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; @@ -54,6 +55,7 @@ private: 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 { diff --git a/plugin/energymanagerimpl.cpp b/plugin/energymanagerimpl.cpp index ea3eef5..56e80d4 100644 --- a/plugin/energymanagerimpl.cpp +++ b/plugin/energymanagerimpl.cpp @@ -142,8 +142,10 @@ void EnergyManagerImpl::watchThing(Thing *thing) || thing->thingClass().interfaces().contains("smartmeterproducer") || thing->thingClass().interfaces().contains("energystorage")) { - m_totalEnergyConsumedCache[thing] = thing->stateValue("totalEnergyConsumed").toDouble(); - m_totalEnergyProducedCache[thing] = thing->stateValue("totalEnergyProduced").toDouble(); + ThingPowerLogEntry entry = m_logger->latestLogEntry(EnergyLogs::SampleRate1Min, {thing->id()}); + m_totalEnergyConsumedCache[thing] = entry.totalConsumption(); + m_totalEnergyProducedCache[thing] = entry.totalProduction(); + qCDebug(dcEnergyExperience()) << "Loaded thing power totals for" << thing->name() << "Consumption:" << entry.totalConsumption() << "Production:" << entry.totalProduction(); connect(thing, &Thing::stateValueChanged, this, [=](const StateTypeId &stateTypeId, const QVariant &value){ if (thing->thingClass().getStateType(stateTypeId).name() == "currentPower") { @@ -171,13 +173,13 @@ void EnergyManagerImpl::updatePowerBalance() double oldAcquisition = m_totalEnergyConsumedCache.value(m_rootMeter); double newAcquisition = m_rootMeter->stateValue("totalEnergyConsumed").toDouble(); - qCDebug(dcEnergyExperience()) << "Root meteter total consumption: Previous value:" << oldAcquisition << "New value:" << newAcquisition << "Diff:" << (newAcquisition -oldAcquisition); + qCDebug(dcEnergyExperience()) << "Root meter total consumption: Previous value:" << oldAcquisition << "New value:" << newAcquisition << "Diff:" << (newAcquisition -oldAcquisition); m_totalAcquisition += newAcquisition - oldAcquisition; m_totalEnergyConsumedCache[m_rootMeter] = newAcquisition; double oldReturn = m_totalEnergyProducedCache.value(m_rootMeter); double newReturn = m_rootMeter->stateValue("totalEnergyProduced").toDouble(); - qCDebug(dcEnergyExperience()) << "Root meteter total return diff" << "old" << oldReturn << " new" << newReturn << (newReturn - oldReturn); + qCDebug(dcEnergyExperience()) << "Root meter total production: Previous value:" << oldReturn << "New value:" << newReturn << "Diff:" << (newReturn - oldReturn); m_totalReturn += newReturn - oldReturn; m_totalEnergyProducedCache[m_rootMeter] = newReturn; } @@ -187,7 +189,7 @@ void EnergyManagerImpl::updatePowerBalance() currentPowerProduction += thing->stateValue("currentPower").toDouble(); double oldProduction = m_totalEnergyProducedCache.value(thing); double newProduction = thing->stateValue("totalEnergyProduced").toDouble(); - qCDebug(dcEnergyExperience()) << "inverter total production diff" << "old" << oldProduction << " new" << newProduction << (newProduction - oldProduction); + qCDebug(dcEnergyExperience()) << "Producer" << thing->name() << "total production: Previous value:" << oldProduction << "New value:" << newProduction << "Diff:" << (newProduction - oldProduction); m_totalProduction += newProduction - oldProduction; m_totalEnergyProducedCache[thing] = newProduction; } @@ -198,6 +200,7 @@ void EnergyManagerImpl::updatePowerBalance() currentPowerStorage += thing->stateValue("currentPower").toDouble(); double oldProduction = m_totalEnergyProducedCache.value(thing); double newProduction = thing->stateValue("totalEnergyProduced").toDouble(); + qCDebug(dcEnergyExperience()) << "Storage" << thing->name() << "total storage: Previous value:" << oldProduction << "New value:" << newProduction << "Diff:" << (newProduction - oldProduction); totalFromStorage += newProduction - oldProduction; m_totalEnergyProducedCache[thing] = newProduction; } From 5f4534b6dcf8bc64603c2eccbd95f6bd5b6a84dd Mon Sep 17 00:00:00 2001 From: Michael Zanetti Date: Sat, 27 Nov 2021 01:15:24 +0100 Subject: [PATCH 11/20] Fix sampling of things totalEnergy states --- plugin/energylogger.cpp | 15 +++++---------- plugin/energylogger.h | 5 ----- 2 files changed, 5 insertions(+), 15 deletions(-) diff --git a/plugin/energylogger.cpp b/plugin/energylogger.cpp index 3b70433..3fff1a5 100644 --- a/plugin/energylogger.cpp +++ b/plugin/energylogger.cpp @@ -287,10 +287,6 @@ void EnergyLogger::sample() qCDebug(dcEnergyExperience()) << "Sampled:" << "🔥:" << medianConsumption << "🌞:" << medianProduction << "💵:" << medianAcquisition << "🔋:" << medianStorage << "Totals:" << "🔥:" << totalConsumption << "🌞:" << totalProduction << "💵↓:" << totalAcquisition << "💵↑:" << totalReturn; insertPowerBalance(sampleEnd, SampleRate1Min, medianConsumption, medianProduction, medianAcquisition, medianStorage, totalConsumption, totalProduction, totalAcquisition, totalReturn); - m_lastSampleTotalConsumption = totalConsumption; - m_lastSampleTotalProducation = totalProduction; - m_lastSampleTotalAcquisition = totalAcquisition; - m_lastSampleTotalReturn = totalReturn; foreach (const ThingId &thingId, m_thingsPowerLiveLogs.keys()) { medianConsumption = 0; @@ -308,12 +304,11 @@ void EnergyLogger::sample() } } medianConsumption /= sampleStart.msecsTo(sampleEnd); - double totalConsumption = 0; - double totalProduction = 0; - if (entries.count() > 0) { - totalConsumption = entries.last().totalConsumption(); - totalProduction = entries.last().totalProduction(); - } + + ThingPowerLogEntry newest = entries.count() > 0 ? entries.first() : ThingPowerLogEntry(); + double totalConsumption = newest.totalConsumption(); + double totalProduction = newest.totalProduction(); + qCDebug(dcEnergyExperience()) << "Sampled:" << "🔥:" << medianConsumption << "🌞:" << medianProduction << "Totals:" << "🔥:" << totalConsumption << "🌞:" << totalProduction; insertThingPower(sampleEnd, SampleRate1Min, thingId, medianConsumption, totalConsumption, totalProduction); } diff --git a/plugin/energylogger.h b/plugin/energylogger.h index f90ed18..07378de 100644 --- a/plugin/energylogger.h +++ b/plugin/energylogger.h @@ -69,11 +69,6 @@ private: QTimer m_sampleTimer; QHash m_nextSamples; - double m_lastSampleTotalConsumption = 0; - double m_lastSampleTotalProducation = 0; - double m_lastSampleTotalAcquisition = 0; - double m_lastSampleTotalReturn = 0; - QSqlDatabase m_db; int m_maxMinuteSamples = 0; From bddaae3d91e46a077fbf8487471e4f3a8954eff8 Mon Sep 17 00:00:00 2001 From: Michael Zanetti Date: Sat, 27 Nov 2021 16:18:52 +0100 Subject: [PATCH 12/20] Some more fixes --- plugin/energylogger.cpp | 134 +++++++++++++++++++++++++++------------- 1 file changed, 92 insertions(+), 42 deletions(-) diff --git a/plugin/energylogger.cpp b/plugin/energylogger.cpp index 3fff1a5..031d21e 100644 --- a/plugin/energylogger.cpp +++ b/plugin/energylogger.cpp @@ -78,7 +78,7 @@ void EnergyLogger::logPowerBalance(double consumption, double production, double void EnergyLogger::logThingPower(const ThingId &thingId, double currentPower, double totalConsumption, double totalProduction) { - qCDebug(dcEnergyExperience()) << "Logging thing power:" << currentPower << totalConsumption << totalProduction; + qCDebug(dcEnergyExperience()) << "Logging thing power for" << thingId.toString() << "Current power:" << currentPower << "Total consumption:" << totalConsumption << "Total production:" << totalProduction; ThingPowerLogEntry entry(QDateTime::currentDateTime(), thingId, currentPower, totalConsumption, totalProduction); m_thingsPowerLiveLogs[thingId].prepend(entry); @@ -285,32 +285,32 @@ void EnergyLogger::sample() double totalAcquisition = newest.totalAcquisition(); double totalReturn = newest.totalReturn(); - qCDebug(dcEnergyExperience()) << "Sampled:" << "🔥:" << medianConsumption << "🌞:" << medianProduction << "💵:" << medianAcquisition << "🔋:" << medianStorage << "Totals:" << "🔥:" << totalConsumption << "🌞:" << totalProduction << "💵↓:" << totalAcquisition << "💵↑:" << 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()) { - medianConsumption = 0; - qCDebug(dcEnergyExperience()) << "Sampling thing energy for 1 min:" << thingId; + 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); - medianConsumption += entry.currentPower() * frameDuration; + medianPower += entry.currentPower() * frameDuration; // qCDebug(dcEnergyExperience()) << "Frame" << i << "duration:" << frameDuration << "value:" << entry.value; if (entry.timestamp() < sampleStart) { break; } } - medianConsumption /= sampleStart.msecsTo(sampleEnd); + medianPower /= sampleStart.msecsTo(sampleEnd); ThingPowerLogEntry newest = entries.count() > 0 ? entries.first() : ThingPowerLogEntry(); double totalConsumption = newest.totalConsumption(); double totalProduction = newest.totalProduction(); - qCDebug(dcEnergyExperience()) << "Sampled:" << "🔥:" << medianConsumption << "🌞:" << medianProduction << "Totals:" << "🔥:" << totalConsumption << "🌞:" << totalProduction; - insertThingPower(sampleEnd, SampleRate1Min, thingId, medianConsumption, totalConsumption, totalProduction); + qCDebug(dcEnergyExperience()) << "Sampled thing power for" << thingId << SampleRate1Min << "🔥/🌞:" << medianPower << "Totals:" << "🔥:" << totalConsumption << "🌞:" << totalProduction; + insertThingPower(sampleEnd, SampleRate1Min, thingId, medianPower, totalConsumption, totalProduction); } } @@ -483,7 +483,7 @@ void EnergyLogger::scheduleNextSample(SampleRate sampleRate) void EnergyLogger::rectifySamples(SampleRate sampleRate, SampleRate baseSampleRate) { - // Normally we'd need to find the newest available sample of a serien and catch up from there. + // 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 at all (regardless of the sampleRate) @@ -575,7 +575,18 @@ bool EnergyLogger::samplePowerBalance(SampleRate sampleRate, SampleRate baseSamp { QDateTime sampleStart = sampleEnd.addMSecs(-sampleRate * 60 * 1000); - qCDebug(dcEnergyExperience()) << "Sampling" << sampleRate << "from" << sampleStart << "to" << sampleEnd; + // FIXME: If base samplerate does not contain a single entry in the given timeframe (e.g. system has been off for more than 15 mins) we seem to mess up totalConsumption + // Needs verifying that lower sample rates are always rectified first! + 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 < ?;"); @@ -590,29 +601,45 @@ bool EnergyLogger::samplePowerBalance(SampleRate sampleRate, SampleRate baseSamp return false; } - double medianConsumption = 0; - double medianProduction = 0; - double medianAcquisition = 0; - double medianStorage = 0; - double totalConsumption = 0; - double totalProduction = 0; - double totalAcquisition = 0; - double totalReturn = 0; - while (query.next()) { - 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 (query.size() > 0) { + while (query.next()) { + 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(); + } + 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(); + } } - medianConsumption = medianConsumption * baseSampleRate / sampleRate; - medianProduction = medianProduction * baseSampleRate / sampleRate; - medianAcquisition = medianAcquisition * baseSampleRate / sampleRate; - medianStorage = medianStorage * baseSampleRate / sampleRate; + qCDebug(dcEnergyExperience()) << "Sampled:" << "🔥:" << medianConsumption << "🌞:" << medianProduction << "💵:" << medianAcquisition << "🔋:" << medianStorage << "Totals:" << "🔥:" << totalConsumption << "🌞:" << totalProduction << "💵↓:" << totalAcquisition << "💵↑:" << totalReturn; return insertPowerBalance(sampleEnd, sampleRate, medianConsumption, medianProduction, medianAcquisition, medianStorage, totalConsumption, totalProduction, totalAcquisition, totalReturn); @@ -654,7 +681,11 @@ bool EnergyLogger::sampleThingPower(const ThingId &thingId, SampleRate sampleRat { QDateTime sampleStart = sampleEnd.addMSecs(-sampleRate * 60 * 1000); - qCDebug(dcEnergyExperience()) << "Sampling" << sampleRate << "from" << sampleStart << "to" << sampleEnd; + qCDebug(dcEnergyExperience()) << "Sampling thing power for" << thingId.toString() << sampleRate << "from" << sampleStart << "to" << sampleEnd; + + 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 < ?;"); @@ -670,16 +701,35 @@ bool EnergyLogger::sampleThingPower(const ThingId &thingId, SampleRate sampleRat return false; } - double medianCurrentPower = 0; - double totalConsumption = 0; - double totalProduction = 0; - while (query.next()) { - 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 (query.size() > 0) { + while (query.next()) { + 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(); + } + 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 sampleRate = ?;"); + 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(); + } } - medianCurrentPower = medianCurrentPower * baseSampleRate / sampleRate; + qCDebug(dcEnergyExperience()) << "Sampled:" << thingId << sampleRate << "media currentpower:" << medianCurrentPower << "total consumption:" << totalConsumption << "total production:" << totalProduction; return insertThingPower(sampleEnd, sampleRate, thingId, medianCurrentPower, totalConsumption, totalProduction); From d494e5a181cd279d0bd67be60bb29b28bbee8fc8 Mon Sep 17 00:00:00 2001 From: Michael Zanetti Date: Sun, 28 Nov 2021 01:40:09 +0100 Subject: [PATCH 13/20] More fixes --- plugin/energylogger.cpp | 52 ++++++++++++++++++++++++----------------- 1 file changed, 30 insertions(+), 22 deletions(-) diff --git a/plugin/energylogger.cpp b/plugin/energylogger.cpp index 031d21e..f7c2f23 100644 --- a/plugin/energylogger.cpp +++ b/plugin/energylogger.cpp @@ -601,18 +601,20 @@ bool EnergyLogger::samplePowerBalance(SampleRate sampleRate, SampleRate baseSamp return false; } - if (query.size() > 0) { - while (query.next()) { - 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(); - } + 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; @@ -681,7 +683,7 @@ bool EnergyLogger::sampleThingPower(const ThingId &thingId, SampleRate sampleRat { QDateTime sampleStart = sampleEnd.addMSecs(-sampleRate * 60 * 1000); - qCDebug(dcEnergyExperience()) << "Sampling thing power for" << thingId.toString() << sampleRate << "from" << sampleStart << "to" << sampleEnd; + qCDebug(dcEnergyExperience()) << "Sampling thing power for" << thingId.toString() << sampleRate << "from" << sampleStart.toString() << "to" << sampleEnd.toString(); double medianCurrentPower = 0; double totalConsumption = 0; @@ -701,13 +703,18 @@ bool EnergyLogger::sampleThingPower(const ThingId &thingId, SampleRate sampleRat return false; } - if (query.size() > 0) { - while (query.next()) { - 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(); - } + 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 { @@ -715,7 +722,8 @@ bool EnergyLogger::sampleThingPower(const ThingId &thingId, SampleRate sampleRat // 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 sampleRate = ?;"); + 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()) { @@ -731,7 +739,7 @@ bool EnergyLogger::sampleThingPower(const ThingId &thingId, SampleRate sampleRat } - qCDebug(dcEnergyExperience()) << "Sampled:" << thingId << sampleRate << "media currentpower:" << medianCurrentPower << "total consumption:" << totalConsumption << "total production:" << totalProduction; + qCDebug(dcEnergyExperience()) << "Sampled:" << thingId.toString() << sampleRate << "median currentPower:" << medianCurrentPower << "total consumption:" << totalConsumption << "total production:" << totalProduction; return insertThingPower(sampleEnd, sampleRate, thingId, medianCurrentPower, totalConsumption, totalProduction); } From abe01d3869e41c86d1c027dc809e826549b0b415 Mon Sep 17 00:00:00 2001 From: Michael Zanetti Date: Sun, 28 Nov 2021 12:14:35 +0100 Subject: [PATCH 14/20] Fix re-initializing things logger --- plugin/energylogger.cpp | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/plugin/energylogger.cpp b/plugin/energylogger.cpp index f7c2f23..ec087b6 100644 --- a/plugin/energylogger.cpp +++ b/plugin/energylogger.cpp @@ -41,9 +41,9 @@ EnergyLogger::EnergyLogger(QObject *parent) : EnergyLogs(parent) addConfig(SampleRate1Month, SampleRate1Day, 240); // 20 years addConfig(SampleRate1Year, SampleRate1Month, 20); // 20 years - // Load thingIds from logs so we have the complete list available for sampling, even if a thing might not produce any logs for a while. + // 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] = ThingPowerLogEntries(); + m_thingsPowerLiveLogs[thingId].append(latestLogEntry(SampleRate1Min, thingId)); } // Start the scheduling @@ -265,14 +265,15 @@ void EnergyLogger::sample() 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(); + + if (entry.timestamp() <= sampleStart) { + break; + } medianConsumption += entry.consumption() * frameDuration; medianProduction += entry.production() * frameDuration; medianAcquisition += entry.acquisition() * frameDuration; medianStorage += entry.storage() * frameDuration; -// qCDebug(dcEnergyExperience()) << "Frame" << i << "duration:" << frameDuration << "value:" << entry.consumption << "start" << frameStart.toString() << "end" << frameEnd.toString(); - if (entry.timestamp() < sampleStart) { - break; - } } medianConsumption /= sampleStart.msecsTo(sampleEnd); medianProduction /= sampleStart.msecsTo(sampleEnd); @@ -297,11 +298,11 @@ void EnergyLogger::sample() QDateTime frameStart = (entry.timestamp() < sampleStart) ? sampleStart : entry.timestamp(); QDateTime frameEnd = i == 0 ? sampleEnd : entries.at(i-1).timestamp(); int frameDuration = frameStart.msecsTo(frameEnd); - medianPower += entry.currentPower() * frameDuration; -// qCDebug(dcEnergyExperience()) << "Frame" << i << "duration:" << frameDuration << "value:" << entry.value; - if (entry.timestamp() < sampleStart) { + qCDebug(dcEnergyExperience()) << "Frame" << i << "duration:" << frameDuration << "value:" << entry.currentPower(); + if (entry.timestamp() <= sampleStart) { break; } + medianPower += entry.currentPower() * frameDuration; } medianPower /= sampleStart.msecsTo(sampleEnd); @@ -589,7 +590,7 @@ bool EnergyLogger::samplePowerBalance(SampleRate sampleRate, SampleRate baseSamp double totalReturn = 0; QSqlQuery query(m_db); - query.prepare("SELECT * FROM powerBalance WHERE sampleRate = ? AND timestamp >= ? AND timestamp < ?;"); + query.prepare("SELECT * FROM powerBalance WHERE sampleRate = ? AND timestamp > ? AND timestamp <= ?;"); query.addBindValue(baseSampleRate); query.addBindValue(sampleStart.toMSecsSinceEpoch()); query.addBindValue(sampleEnd.toMSecsSinceEpoch()); @@ -690,7 +691,7 @@ bool EnergyLogger::sampleThingPower(const ThingId &thingId, SampleRate sampleRat double totalProduction = 0; QSqlQuery query(m_db); - query.prepare("SELECT * FROM thingPower WHERE thingId = ? AND sampleRate = ? AND timestamp >= ? AND timestamp < ?;"); + query.prepare("SELECT * FROM thingPower WHERE thingId = ? AND sampleRate = ? AND timestamp > ? AND timestamp <= ?;"); query.addBindValue(thingId); query.addBindValue(baseSampleRate); query.addBindValue(sampleStart.toMSecsSinceEpoch()); From 97286a5827172dc1542107d71fcf361fd002e654 Mon Sep 17 00:00:00 2001 From: Michael Zanetti Date: Mon, 29 Nov 2021 14:33:16 +0100 Subject: [PATCH 15/20] More fixes --- plugin/energylogger.cpp | 10 +++++----- plugin/energymanagerimpl.cpp | 6 +++--- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/plugin/energylogger.cpp b/plugin/energylogger.cpp index ec087b6..d8d18e7 100644 --- a/plugin/energylogger.cpp +++ b/plugin/energylogger.cpp @@ -267,13 +267,13 @@ void EnergyLogger::sample() int frameDuration = frameStart.msecsTo(frameEnd); qCDebug(dcEnergyExperience()) << "Frame" << i << "duration:" << frameDuration << "value:" << entry.consumption() << "start" << frameStart.toString() << "end" << frameEnd.toString(); - if (entry.timestamp() <= sampleStart) { - break; - } 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); @@ -299,10 +299,10 @@ void EnergyLogger::sample() QDateTime frameEnd = i == 0 ? sampleEnd : entries.at(i-1).timestamp(); int frameDuration = frameStart.msecsTo(frameEnd); qCDebug(dcEnergyExperience()) << "Frame" << i << "duration:" << frameDuration << "value:" << entry.currentPower(); - if (entry.timestamp() <= sampleStart) { + medianPower += entry.currentPower() * frameDuration; + if (entry.timestamp() < sampleStart) { break; } - medianPower += entry.currentPower() * frameDuration; } medianPower /= sampleStart.msecsTo(sampleEnd); diff --git a/plugin/energymanagerimpl.cpp b/plugin/energymanagerimpl.cpp index 56e80d4..dc9b849 100644 --- a/plugin/energymanagerimpl.cpp +++ b/plugin/energymanagerimpl.cpp @@ -147,9 +147,9 @@ void EnergyManagerImpl::watchThing(Thing *thing) m_totalEnergyProducedCache[thing] = entry.totalProduction(); qCDebug(dcEnergyExperience()) << "Loaded thing power totals for" << thing->name() << "Consumption:" << entry.totalConsumption() << "Production:" << entry.totalProduction(); - connect(thing, &Thing::stateValueChanged, this, [=](const StateTypeId &stateTypeId, const QVariant &value){ - if (thing->thingClass().getStateType(stateTypeId).name() == "currentPower") { - m_logger->logThingPower(thing->id(), value.toDouble(), thing->state("totalEnergyConsumed").value().toDouble(), thing->state("totalEnergyProduced").value().toDouble()); + connect(thing, &Thing::stateValueChanged, this, [=](const StateTypeId &stateTypeId, const QVariant &/*value*/){ + if (QStringList({"currentPower", "totalEnergyConsumed", "totalEnergyProduced"}).contains(thing->thingClass().getStateType(stateTypeId).name())) { + m_logger->logThingPower(thing->id(), thing->state("currentPower").value().toDouble(), thing->state("totalEnergyConsumed").value().toDouble(), thing->state("totalEnergyProduced").value().toDouble()); } }); } From 2df41462ad67f90265f52ebd38e47506ab9bad8c Mon Sep 17 00:00:00 2001 From: Michael Zanetti Date: Mon, 6 Dec 2021 12:24:34 +0100 Subject: [PATCH 16/20] Update size estimation comments based on new log entry format --- plugin/energylogger.cpp | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/plugin/energylogger.cpp b/plugin/energylogger.cpp index d8d18e7..9f1193c 100644 --- a/plugin/energylogger.cpp +++ b/plugin/energylogger.cpp @@ -25,11 +25,12 @@ EnergyLogger::EnergyLogger(QObject *parent) : EnergyLogs(parent) // of all series building on it. // Disk space considerations; - // Each entry takes approx 30 bytes for powerBalance + 50 bytes for thingCurrentPower per thing of disk space + // 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 * 30 bytes) + (count * things * 50 bytes) + 5% - // 10000 entries, with 5 energy things => ~3MB + // (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; From 2da2d9458b5b834eb0f7f7060977d85749e12d86 Mon Sep 17 00:00:00 2001 From: Michael Zanetti Date: Tue, 7 Dec 2021 23:48:23 +0100 Subject: [PATCH 17/20] Cleanups --- libnymea-energy/energylogs.h | 2 +- plugin/energylogger.cpp | 4 +--- plugin/energymanagerimpl.cpp | 11 ++++++----- 3 files changed, 8 insertions(+), 9 deletions(-) diff --git a/libnymea-energy/energylogs.h b/libnymea-energy/energylogs.h index 79c89a2..301b6e2 100644 --- a/libnymea-energy/energylogs.h +++ b/libnymea-energy/energylogs.h @@ -38,7 +38,7 @@ public: virtual PowerBalanceLogEntries powerBalanceLogs(SampleRate sampleRate, const QDateTime &from = QDateTime(), const QDateTime &to = QDateTime()) const = 0; /*! Returns logs for the given sample rate for currentPower, totalEnergyConsumed and totalEnergyProduced for the given things. - * From and to may be given to limie results to a time span. + * 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; diff --git a/plugin/energylogger.cpp b/plugin/energylogger.cpp index 9f1193c..9119a6d 100644 --- a/plugin/energylogger.cpp +++ b/plugin/energylogger.cpp @@ -488,7 +488,7 @@ void EnergyLogger::rectifySamples(SampleRate sampleRate, SampleRate baseSampleRa // 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 at all (regardless of the sampleRate) + // 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); @@ -577,8 +577,6 @@ bool EnergyLogger::samplePowerBalance(SampleRate sampleRate, SampleRate baseSamp { QDateTime sampleStart = sampleEnd.addMSecs(-sampleRate * 60 * 1000); - // FIXME: If base samplerate does not contain a single entry in the given timeframe (e.g. system has been off for more than 15 mins) we seem to mess up totalConsumption - // Needs verifying that lower sample rates are always rectified first! qCDebug(dcEnergyExperience()) << "Sampling power balance" << sampleRate << "from" << sampleStart << "to" << sampleEnd; double medianConsumption = 0; diff --git a/plugin/energymanagerimpl.cpp b/plugin/energymanagerimpl.cpp index dc9b849..ea664f3 100644 --- a/plugin/energymanagerimpl.cpp +++ b/plugin/energymanagerimpl.cpp @@ -12,10 +12,11 @@ EnergyManagerImpl::EnergyManagerImpl(ThingManager *thingManager, QObject *parent m_thingManager(thingManager), m_logger(new EnergyLogger(this)) { - // Most of the time we get a bunch of signals at the same time (root meter, producers, consumers etc) - // In order to decrease some load on the system, we'll wait for wee bit until we actually update to - // accumulate those changes and calculate the change in one go. - m_balanceUpdateTimer.setInterval(50); + // 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); @@ -125,7 +126,7 @@ void EnergyManagerImpl::watchThing(Thing *thing) qCDebug(dcEnergyExperience()) << "Wathing thing:" << thing->name(); - // React on things that requie us updating the power balance + // 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")) { From 68bc1d648fd437828727a200ae405298407a593d Mon Sep 17 00:00:00 2001 From: Michael Zanetti Date: Sat, 11 Dec 2021 00:28:43 +0100 Subject: [PATCH 18/20] Make internal counters independent from thing counters --- plugin/energylogger.cpp | 67 +++++++++++++++++++- plugin/energylogger.h | 6 ++ plugin/energymanagerimpl.cpp | 114 ++++++++++++++++++++++++++++++----- plugin/energymanagerimpl.h | 16 ++++- 4 files changed, 186 insertions(+), 17 deletions(-) diff --git a/plugin/energylogger.cpp b/plugin/energylogger.cpp index 9119a6d..6ddfff6 100644 --- a/plugin/energylogger.cpp +++ b/plugin/energylogger.cpp @@ -79,7 +79,6 @@ void EnergyLogger::logPowerBalance(double consumption, double production, double void EnergyLogger::logThingPower(const ThingId &thingId, double currentPower, double totalConsumption, double totalProduction) { - qCDebug(dcEnergyExperience()) << "Logging thing power for" << thingId.toString() << "Current power:" << currentPower << "Total consumption:" << totalConsumption << "Total production:" << totalProduction; ThingPowerLogEntry entry(QDateTime::currentDateTime(), thingId, currentPower, totalConsumption, totalProduction); m_thingsPowerLiveLogs[thingId].prepend(entry); @@ -177,6 +176,11 @@ ThingPowerLogEntries EnergyLogger::thingPowerLogs(SampleRate sampleRate, const Q 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; @@ -203,6 +207,12 @@ PowerBalanceLogEntry EnergyLogger::latestLogEntry(SampleRate sampleRate) 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); @@ -247,6 +257,36 @@ QList EnergyLogger::loggedThings() const 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(); @@ -375,6 +415,17 @@ bool EnergyLogger::initDB() 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 " @@ -414,6 +465,20 @@ bool EnergyLogger::initDB() } } + 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; } diff --git a/plugin/energylogger.h b/plugin/energylogger.h index 07378de..6c279d8 100644 --- a/plugin/energylogger.h +++ b/plugin/energylogger.h @@ -30,6 +30,12 @@ public: 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(); diff --git a/plugin/energymanagerimpl.cpp b/plugin/energymanagerimpl.cpp index ea664f3..5eca4c5 100644 --- a/plugin/energymanagerimpl.cpp +++ b/plugin/energymanagerimpl.cpp @@ -124,7 +124,7 @@ void EnergyManagerImpl::watchThing(Thing *thing) setRootMeter(thing->id()); } - qCDebug(dcEnergyExperience()) << "Wathing thing:" << thing->name(); + qCDebug(dcEnergyExperience()) << "Watching thing:" << thing->name(); // React on things that require us updating the power balance if (thing->thingClass().interfaces().contains("energymeter") @@ -143,14 +143,69 @@ void EnergyManagerImpl::watchThing(Thing *thing) || thing->thingClass().interfaces().contains("smartmeterproducer") || thing->thingClass().interfaces().contains("energystorage")) { - ThingPowerLogEntry entry = m_logger->latestLogEntry(EnergyLogs::SampleRate1Min, {thing->id()}); - m_totalEnergyConsumedCache[thing] = entry.totalConsumption(); - m_totalEnergyProducedCache[thing] = entry.totalProduction(); - qCDebug(dcEnergyExperience()) << "Loaded thing power totals for" << thing->name() << "Consumption:" << entry.totalConsumption() << "Production:" << entry.totalProduction(); + // 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())) { - m_logger->logThingPower(thing->id(), thing->state("currentPower").value().toDouble(), thing->state("totalEnergyConsumed").value().toDouble(), thing->state("totalEnergyProduced").value().toDouble()); + + // 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); } }); } @@ -172,38 +227,69 @@ void EnergyManagerImpl::updatePowerBalance() if (m_rootMeter) { currentPowerAcquisition = m_rootMeter->stateValue("currentPower").toDouble(); - double oldAcquisition = m_totalEnergyConsumedCache.value(m_rootMeter); + 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_totalEnergyConsumedCache[m_rootMeter] = newAcquisition; + m_powerBalanceTotalEnergyConsumedCache[m_rootMeter] = newAcquisition; - double oldReturn = m_totalEnergyProducedCache.value(m_rootMeter); + 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_totalEnergyProducedCache[m_rootMeter] = newReturn; + m_powerBalanceTotalEnergyProducedCache[m_rootMeter] = newReturn; } double currentPowerProduction = 0; foreach (Thing* thing, m_thingManager->configuredThings().filterByInterface("smartmeterproducer")) { currentPowerProduction += thing->stateValue("currentPower").toDouble(); - double oldProduction = m_totalEnergyProducedCache.value(thing); + 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_totalEnergyProducedCache[thing] = newProduction; + 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_totalEnergyProducedCache.value(thing); + 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_totalEnergyProducedCache[thing] = newProduction; + m_powerBalanceTotalEnergyProducedCache[thing] = newProduction; } double currentPowerConsumption = currentPowerAcquisition + qAbs(qMin(0.0, currentPowerProduction)) - currentPowerStorage; diff --git a/plugin/energymanagerimpl.h b/plugin/energymanagerimpl.h index 6b172a9..8964e9e 100644 --- a/plugin/energymanagerimpl.h +++ b/plugin/energymanagerimpl.h @@ -57,8 +57,20 @@ private: EnergyLogger *m_logger = nullptr; - QHash m_totalEnergyConsumedCache; - QHash m_totalEnergyProducedCache; + // 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 From 70ad30ccd3e6082e3ef990c8e85a5d28c2b2159a Mon Sep 17 00:00:00 2001 From: Michael Zanetti Date: Sun, 12 Dec 2021 16:44:10 +0100 Subject: [PATCH 19/20] Add option to include the current "live" value in thing power logs --- plugin/energyjsonhandler.cpp | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/plugin/energyjsonhandler.cpp b/plugin/energyjsonhandler.cpp index 052266e..dc2a75e 100644 --- a/plugin/energyjsonhandler.cpp +++ b/plugin/energyjsonhandler.cpp @@ -51,11 +51,17 @@ EnergyJsonHandler::EnergyJsonHandler(EnergyManager *energyManager, QObject *pare 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."; + "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); @@ -78,13 +84,13 @@ EnergyJsonHandler::EnergyJsonHandler(EnergyManager *energyManager, QObject *pare registerNotification("PowerBalanceChanged", description, params); params.clear(); - description = "Emitted whenever a entry is added to the power balance log."; + 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 a entry is added to the thing power log."; + description = "Emitted whenever an entry is added to the thing power log."; params.insert("sampleRate", enumRef()); params.insert("thingPowerLogEntry", objectRef()); registerNotification("ThingPowerLogEntryAdded", description, params); @@ -189,5 +195,10 @@ JsonReply *EnergyJsonHandler::GetThingPowerLogs(const QVariantMap ¶ms) 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); } From c0146c0abf27c25176842c9dbf54168eeffe0dcb Mon Sep 17 00:00:00 2001 From: Michael Zanetti Date: Tue, 14 Dec 2021 13:51:25 +0100 Subject: [PATCH 20/20] Fix total caluclation (subtract return from consumption) --- plugin/energymanagerimpl.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugin/energymanagerimpl.cpp b/plugin/energymanagerimpl.cpp index 5eca4c5..7b20d00 100644 --- a/plugin/energymanagerimpl.cpp +++ b/plugin/energymanagerimpl.cpp @@ -293,7 +293,7 @@ void EnergyManagerImpl::updatePowerBalance() } double currentPowerConsumption = currentPowerAcquisition + qAbs(qMin(0.0, currentPowerProduction)) - currentPowerStorage; - m_totalConsumption = m_totalAcquisition + m_totalProduction + totalFromStorage; + 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