Merge PR #1: Add nymea energy experience plugin

This commit is contained in:
Jenkins nymea 2021-12-14 14:04:03 +01:00
commit dbb8885009
26 changed files with 2469 additions and 0 deletions

2
.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
*.pro.user
.crossbuilder/*

10
config.pri Normal file
View File

@ -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

3
debian/changelog vendored Normal file
View File

@ -0,0 +1,3 @@
nymea-experience-plugin-energy (0.3) xenial; urgency=medium
-- Jenkins <jenkins@nymea.io> Tue, 05 Oct 2021 08:33:51 +0200

1
debian/compat vendored Normal file
View File

@ -0,0 +1 @@
9

42
debian/control vendored Normal file
View File

@ -0,0 +1,42 @@
Source: nymea-experience-plugin-energy
Section: utils
Priority: optional
Maintainer: Michael Zanetti <michael.zanetti@nymea.io>
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

3
debian/libnymea-energy-dev.install.in vendored Normal file
View File

@ -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

3
debian/libnymea-energy.install.in vendored Normal file
View File

@ -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

View File

@ -0,0 +1 @@
usr/lib/@DEB_HOST_MULTIARCH@/nymea/experiences/libnymea_experiencepluginenergy.so

27
debian/rules vendored Executable file
View File

@ -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

View File

@ -0,0 +1,132 @@
#include "energylogs.h"
#include <QVariant>
EnergyLogs::EnergyLogs(QObject *parent): QObject(parent)
{
}
PowerBalanceLogEntry::PowerBalanceLogEntry()
{
}
PowerBalanceLogEntry::PowerBalanceLogEntry(const QDateTime &timestamp, double consumption, double production, double acquisition, double storage, double totalConsumption, double totalProduction, double totalAcquisition, double totalReturn):
m_timestamp(timestamp),
m_consumption(consumption),
m_production(production),
m_acquisition(acquisition),
m_storage(storage),
m_totalConsumption(totalConsumption),
m_totalProduction(totalProduction),
m_totalAcquisition(totalAcquisition),
m_totalReturn(totalReturn)
{
}
QDateTime PowerBalanceLogEntry::timestamp() const
{
return m_timestamp;
}
double PowerBalanceLogEntry::consumption() const
{
return m_consumption;
}
double PowerBalanceLogEntry::production() const
{
return m_production;
}
double PowerBalanceLogEntry::acquisition() const
{
return m_acquisition;
}
double PowerBalanceLogEntry::storage() const
{
return m_storage;
}
double PowerBalanceLogEntry::totalConsumption() const
{
return m_totalConsumption;
}
double PowerBalanceLogEntry::totalProduction() const
{
return m_totalProduction;
}
double PowerBalanceLogEntry::totalAcquisition() const
{
return m_totalAcquisition;
}
double PowerBalanceLogEntry::totalReturn() const
{
return m_totalReturn;
}
QVariant PowerBalanceLogEntries::get(int index) const
{
return QVariant::fromValue(at(index));
}
void PowerBalanceLogEntries::put(const QVariant &variant)
{
append(variant.value<PowerBalanceLogEntry>());
}
ThingPowerLogEntry::ThingPowerLogEntry()
{
}
ThingPowerLogEntry::ThingPowerLogEntry(const QDateTime &timestamp, const ThingId &thingId, double currentPower, double totalConsumption, double totalProduction):
m_timestamp(timestamp),
m_thingId(thingId),
m_currentPower(currentPower),
m_totalConsumption(totalConsumption),
m_totalProduction(totalProduction)
{
}
QDateTime ThingPowerLogEntry::timestamp() const
{
return m_timestamp;
}
ThingId ThingPowerLogEntry::thingId() const
{
return m_thingId;
}
double ThingPowerLogEntry::currentPower() const
{
return m_currentPower;
}
double ThingPowerLogEntry::totalConsumption() const
{
return m_totalConsumption;
}
double ThingPowerLogEntry::totalProduction() const
{
return m_totalProduction;
}
QVariant ThingPowerLogEntries::get(int index) const
{
return QVariant::fromValue(at(index));
}
void ThingPowerLogEntries::put(const QVariant &variant)
{
append(variant.value<ThingPowerLogEntry>());
}

View File

@ -0,0 +1,139 @@
#ifndef ENERGYLOGS_H
#define ENERGYLOGS_H
#include <QObject>
#include <QDateTime>
#include <QVariant>
#include <typeutils.h>
class PowerBalanceLogEntry;
class PowerBalanceLogEntries;
class ThingPowerLogEntry;
class ThingPowerLogEntries;
class EnergyLogs: public QObject
{
Q_OBJECT
public:
EnergyLogs(QObject *parent = nullptr);
virtual ~EnergyLogs() = default;
enum SampleRate {
SampleRateAny = 0,
SampleRate1Min = 1,
SampleRate15Mins = 15,
SampleRate1Hour = 60,
SampleRate3Hours = 180,
SampleRate1Day = 1440,
SampleRate1Week = 10080,
SampleRate1Month = 43200,
SampleRate1Year = 525600
};
Q_ENUM(SampleRate)
/*! Returns logs for the given sample rate for total household consumption, production, acquisition and storage balance.
* From and to may be given to limit results to a time span.
*/
virtual PowerBalanceLogEntries powerBalanceLogs(SampleRate sampleRate, const QDateTime &from = QDateTime(), const QDateTime &to = QDateTime()) const = 0;
/*! Returns logs for the given sample rate for currentPower, totalEnergyConsumed and totalEnergyProduced for the given things.
* From and to may be given to limit results to a time span.
* If thingIds is empty, all things will be returned.
*/
virtual ThingPowerLogEntries thingPowerLogs(SampleRate sampleRate, const QList<ThingId> &thingIds, const QDateTime &from = QDateTime(), const QDateTime &to = QDateTime()) const = 0;
signals:
void powerBalanceEntryAdded(SampleRate sampleRate, const PowerBalanceLogEntry &entry);
void thingPowerEntryAdded(SampleRate sampleRate, const ThingPowerLogEntry &entry);
};
class PowerBalanceLogEntry
{
Q_GADGET
Q_PROPERTY(QDateTime timestamp READ timestamp)
Q_PROPERTY(double consumption READ consumption)
Q_PROPERTY(double production READ production)
Q_PROPERTY(double acquisition READ acquisition)
Q_PROPERTY(double storage READ storage)
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 &timestamp, double consumption, double production, double acquisition, double storage, double totalConsumption, double totalProduction, double totalAcquisition, double totalReturn);
QDateTime timestamp() const;
double consumption() const;
double production() const;
double acquisition() const;
double storage() const;
double totalConsumption() const;
double totalProduction() const;
double totalAcquisition() const;
double totalReturn() const;
private:
QDateTime m_timestamp;
double m_consumption = 0;
double m_production = 0;
double m_acquisition = 0;
double m_storage = 0;
double m_totalConsumption = 0;
double m_totalProduction = 0;
double m_totalAcquisition = 0;
double m_totalReturn = 0;
};
Q_DECLARE_METATYPE(PowerBalanceLogEntry)
class PowerBalanceLogEntries: public QList<PowerBalanceLogEntry>
{
Q_GADGET
Q_PROPERTY(int count READ count)
public:
PowerBalanceLogEntries() = default;
PowerBalanceLogEntries(const QList<PowerBalanceLogEntry> &other);
Q_INVOKABLE QVariant get(int index) const;
Q_INVOKABLE void put(const QVariant &variant);
};
Q_DECLARE_METATYPE(PowerBalanceLogEntries)
class ThingPowerLogEntry {
Q_GADGET
Q_PROPERTY(QDateTime timestamp READ timestamp)
Q_PROPERTY(QUuid thingId READ thingId)
Q_PROPERTY(double currentPower READ currentPower)
Q_PROPERTY(double totalConsumption READ totalConsumption)
Q_PROPERTY(double totalProduction READ totalProduction)
public:
ThingPowerLogEntry();
ThingPowerLogEntry(const QDateTime &timestamp, const ThingId &thingId, double currentPower, double totalConsumption, double totalProuction);
QDateTime timestamp() const;
ThingId thingId() const;
double currentPower() const;
double totalConsumption() const;
double totalProduction() const;
private:
QDateTime m_timestamp;
ThingId m_thingId;
double m_currentPower = 0;
double m_totalConsumption = 0;
double m_totalProduction = 0;
};
Q_DECLARE_METATYPE(ThingPowerLogEntry)
class ThingPowerLogEntries: public QList<ThingPowerLogEntry>
{
Q_GADGET
Q_PROPERTY(int count READ count)
public:
ThingPowerLogEntries() = default;
ThingPowerLogEntries(const QList<ThingPowerLogEntry> &other): QList<ThingPowerLogEntry>(other) {}
Q_INVOKABLE QVariant get(int index) const;
Q_INVOKABLE void put(const QVariant &variant);
};
Q_DECLARE_METATYPE(ThingPowerLogEntries)
#endif // ENERGYLOGS_H

View File

@ -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 <https://www.gnu.org/licenses/>.
*
* For any further details and any questions please contact us under
* contact@nymea.io or see our FAQ/Licensing Information on
* https://nymea.io/license/faq
*
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
#include "energymanager.h"
EnergyManager::EnergyManager(QObject *parent) : QObject(parent)
{
}

View File

@ -0,0 +1,76 @@
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
*
* Copyright 2013 - 2021, nymea GmbH
* Contact: contact@nymea.io
*
* This file is part of nymea.
* This project including source code and documentation is protected by
* copyright law, and remains the property of nymea GmbH. All rights, including
* reproduction, publication, editing and translation, are reserved. The use of
* this project is subject to the terms of a license agreement to be concluded
* with nymea GmbH in accordance with the terms of use of nymea GmbH, available
* under https://nymea.io/license
*
* GNU General Public License Usage
* Alternatively, this project may be redistributed and/or modified under the
* terms of the GNU General Public License as published by the Free Software
* Foundation, GNU version 3. This project is distributed in the hope that it
* will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty
* of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along with
* this project. If not, see <https://www.gnu.org/licenses/>.
*
* For any further details and any questions please contact us under
* contact@nymea.io or see our FAQ/Licensing Information on
* https://nymea.io/license/faq
*
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
#ifndef ENERGYMANAGER_H
#define ENERGYMANAGER_H
#include "energylogs.h"
#include <QObject>
#include <integrations/thing.h>
class EnergyManager : public QObject
{
Q_OBJECT
public:
enum EnergyError {
EnergyErrorNoError,
EnergyErrorMissingParameter,
EnergyErrorInvalidParameter,
};
Q_ENUM(EnergyError)
explicit EnergyManager(QObject *parent = nullptr);
virtual ~EnergyManager() = default;
virtual EnergyError setRootMeter(const ThingId &rootMeterId) = 0;
virtual Thing *rootMeter() const = 0;
virtual double currentPowerConsumption() const = 0;
virtual double currentPowerProduction() const = 0;
virtual double currentPowerAcquisition() const = 0;
virtual double currentPowerStorage() const = 0;
virtual double totalConsumption() const = 0;
virtual double totalProduction() const = 0;
virtual double totalAcquisition() const = 0;
virtual double totalReturn() const = 0;
virtual EnergyLogs* logs() const = 0;
signals:
void rootMeterChanged();
void powerBalanceChanged();
};
#endif // ENERGYMANAGER_H

View File

@ -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 <https://www.gnu.org/licenses/>.
*
* For any further details and any questions please contact us under
* contact@nymea.io or see our FAQ/Licensing Information on
* https://nymea.io/license/faq
*
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
#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();
}

View File

@ -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 <https://www.gnu.org/licenses/>.
*
* For any further details and any questions please contact us under
* contact@nymea.io or see our FAQ/Licensing Information on
* https://nymea.io/license/faq
*
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
#ifndef ENERGYPLUGIN_H
#define ENERGYPLUGIN_H
#include <QObject>
#include <jsonrpc/jsonrpcserver.h>
#include <integrations/thingmanager.h>
#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

View File

@ -0,0 +1,40 @@
TEMPLATE = lib
TARGET = $$qtLibraryTarget(nymea-energy)
include(../config.pri)
NYMEA_ENERGY_VERSION_STRING = "0.0.1"
CONFIG += link_pkgconfig
PKGCONFIG += nymea
HEADERS += \
energylogs.h \
energymanager.h \
energyplugin.h
SOURCES += \
energylogs.cpp \
energymanager.cpp \
energyplugin.cpp
target.path = $$[QT_INSTALL_LIBS]
INSTALLS += target
for(header, HEADERS) {
path = $$[QT_INSTALL_PREFIX]/include/nymea-energy/$${dirname(header)}
eval(headers_$${path}.files += $${header})
eval(headers_$${path}.path = $${path})
eval(INSTALLS *= headers_$${path})
}
CONFIG += create_pc create_prl no_install_prl
QMAKE_PKGCONFIG_NAME = libnymea-energy
QMAKE_PKGCONFIG_DESCRIPTION = nymea energy library
QMAKE_PKGCONFIG_PREFIX = $$[QT_INSTALL_PREFIX]
QMAKE_PKGCONFIG_INCDIR = $$[QT_INSTALL_PREFIX]/include/nymea-energy/
QMAKE_PKGCONFIG_LIBDIR = $$target.path
QMAKE_PKGCONFIG_VERSION = $$NYMEA_ENERGY_VERSION_STRING
QMAKE_PKGCONFIG_FILE = nymea-energy
QMAKE_PKGCONFIG_DESTDIR = pkgconfig

View File

@ -0,0 +1,7 @@
TEMPLATE = subdirs
SUBDIRS += libnymea-energy plugin
plugin.depends = libnymea-energy

View File

@ -0,0 +1,204 @@
#include "energyjsonhandler.h"
#include "energymanagerimpl.h"
Q_DECLARE_LOGGING_CATEGORY(dcEnergyExperience)
EnergyJsonHandler::EnergyJsonHandler(EnergyManager *energyManager, QObject *parent):
JsonHandler(parent),
m_energyManager(energyManager)
{
registerEnum<EnergyManager::EnergyError>();
registerEnum<EnergyLogs::SampleRate>();
registerObject<PowerBalanceLogEntry, PowerBalanceLogEntries>();
registerObject<ThingPowerLogEntry, ThingPowerLogEntries>();
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<EnergyManager::EnergyError>());
registerMethod("SetRootMeter", description, params, returns);
params.clear(); returns.clear();
description = "Get the current power balance. That is, production, consumption and acquisition.";
returns.insert("currentPowerConsumption", enumValueName(Double));
returns.insert("currentPowerProduction", enumValueName(Double));
returns.insert("currentPowerAcquisition", enumValueName(Double));
returns.insert("currentPowerStorage", enumValueName(Double));
returns.insert("totalConsumption", enumValueName(Double));
returns.insert("totalProduction", enumValueName(Double));
returns.insert("totalAcquisition", enumValueName(Double));
returns.insert("totalReturn", enumValueName(Double));
registerMethod("GetPowerBalance", description, params, returns);
params.clear(); returns.clear();
description = "Get logs for the power balance. If from is not give, the log will start at the beginning of "
"recording. If to is not given, the logs will and at the last sample for this sample rate before now.";
params.insert("sampleRate", enumRef<EnergyLogs::SampleRate>());
params.insert("o:from", enumValueName(Uint));
params.insert("o:to", enumValueName(Uint));
returns.insert("powerBalanceLogEntries", objectRef<PowerBalanceLogEntries>());
registerMethod("GetPowerBalanceLogs", description, params, returns);
params.clear(); returns.clear();
description = "Get logs for one or more things power values. If thingIds is not given, logs for all energy related "
"things will be returned. If from is not given, the log will start at the beginning of recording. If "
"to is not given, the logs will and at the last sample for this sample rate before now. 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<EnergyLogs::SampleRate>());
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<ThingPowerLogEntries>());
returns.insert("thingPowerLogEntries", objectRef<ThingPowerLogEntries>());
registerMethod("GetThingPowerLogs", description, params, returns);
params.clear();
description = "Emitted whenever the root meter id changes. If the root meter has been unset, the params will be empty.";
params.insert("o:rootMeterThingId", enumValueName(Uuid));
registerNotification("RootMeterChanged", description, params);
params.clear();
description = "Emitted whenever the energy balance changes. That is, when the current consumption, production or "
"acquisition changes. Typically they will all change at the same time.";
params.insert("currentPowerConsumption", enumValueName(Double));
params.insert("currentPowerProduction", enumValueName(Double));
params.insert("currentPowerAcquisition", enumValueName(Double));
params.insert("currentPowerStorage", enumValueName(Double));
params.insert("totalConsumption", enumValueName(Double));
params.insert("totalProduction", enumValueName(Double));
params.insert("totalAcquisition", enumValueName(Double));
params.insert("totalReturn", enumValueName(Double));
registerNotification("PowerBalanceChanged", description, params);
params.clear();
description = "Emitted whenever an entry is added to the power balance log.";
params.insert("sampleRate", enumRef<EnergyLogs::SampleRate>());
params.insert("powerBalanceLogEntry", objectRef<PowerBalanceLogEntry>());
registerNotification("PowerBalanceLogEntryAdded", description, params);
params.clear();
description = "Emitted whenever an entry is added to the thing power log.";
params.insert("sampleRate", enumRef<EnergyLogs::SampleRate>());
params.insert("thingPowerLogEntry", objectRef<ThingPowerLogEntry>());
registerNotification("ThingPowerLogEntryAdded", description, params);
connect(m_energyManager, &EnergyManager::rootMeterChanged, this, [=](){
QVariantMap params;
if (m_energyManager->rootMeter()) {
params.insert("rootMeterThingId", m_energyManager->rootMeter()->id());
}
emit RootMeterChanged(params);
});
connect(m_energyManager, &EnergyManager::powerBalanceChanged, this, [=](){
QVariantMap params;
params.insert("currentPowerConsumption", m_energyManager->currentPowerConsumption());
params.insert("currentPowerProduction", m_energyManager->currentPowerProduction());
params.insert("currentPowerAcquisition", m_energyManager->currentPowerAcquisition());
params.insert("currentPowerStorage", m_energyManager->currentPowerStorage());
params.insert("totalConsumption", m_energyManager->totalConsumption());
params.insert("totalProduction", m_energyManager->totalProduction());
params.insert("totalAcquisition", m_energyManager->totalAcquisition());
params.insert("totalReturn", m_energyManager->totalReturn());
emit PowerBalanceChanged(params);
});
connect(m_energyManager->logs(), &EnergyLogs::powerBalanceEntryAdded, this, [=](EnergyLogs::SampleRate sampleRate, const PowerBalanceLogEntry &entry){
QVariantMap params;
params.insert("sampleRate", enumValueName(sampleRate));
params.insert("powerBalanceLogEntry", pack(entry));
emit PowerBalanceLogEntryAdded(params);
});
connect(m_energyManager->logs(), &EnergyLogs::thingPowerEntryAdded, this, [=](EnergyLogs::SampleRate sampleRate, const ThingPowerLogEntry &entry){
QVariantMap params;
params.insert("sampleRate", enumValueName(sampleRate));
params.insert("thingPowerLogEntry", pack(entry));
emit ThingPowerLogEntryAdded(params);
});
}
QString EnergyJsonHandler::name() const
{
return "Energy";
}
JsonReply *EnergyJsonHandler::GetRootMeter(const QVariantMap &params)
{
Q_UNUSED(params)
QVariantMap ret;
if (m_energyManager->rootMeter()) {
ret.insert("rootMeterThingId", m_energyManager->rootMeter()->id());
}
return createReply(ret);
}
JsonReply *EnergyJsonHandler::SetRootMeter(const QVariantMap &params)
{
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 &params)
{
Q_UNUSED(params)
QVariantMap ret;
ret.insert("currentPowerConsumption", m_energyManager->currentPowerConsumption());
ret.insert("currentPowerProduction", m_energyManager->currentPowerProduction());
ret.insert("currentPowerAcquisition", m_energyManager->currentPowerAcquisition());
ret.insert("currentPowerStorage", m_energyManager->currentPowerStorage());
ret.insert("totalConsumption", m_energyManager->totalConsumption());
ret.insert("totalProduction", m_energyManager->totalProduction());
ret.insert("totalAcquisition", m_energyManager->totalAcquisition());
ret.insert("totalReturn", m_energyManager->totalReturn());
return createReply(ret);
}
JsonReply *EnergyJsonHandler::GetPowerBalanceLogs(const QVariantMap &params)
{
EnergyLogs::SampleRate sampleRate = enumNameToValue<EnergyLogs::SampleRate>(params.value("sampleRate").toString());
QDateTime from = params.contains("from") ? QDateTime::fromMSecsSinceEpoch(params.value("from").toLongLong() * 1000) : QDateTime();
QDateTime to = params.contains("to") ? QDateTime::fromMSecsSinceEpoch(params.value("to").toLongLong() * 1000) : QDateTime();
QVariantMap returns;
returns.insert("powerBalanceLogEntries", pack(m_energyManager->logs()->powerBalanceLogs(sampleRate, from, to)));
return createReply(returns);
}
JsonReply *EnergyJsonHandler::GetThingPowerLogs(const QVariantMap &params)
{
EnergyLogs::SampleRate sampleRate = enumNameToValue<EnergyLogs::SampleRate>(params.value("sampleRate").toString());
QList<ThingId> thingIds;
foreach (const QVariant &thingId, params.value("thingIds").toList()) {
thingIds.append(thingId.toUuid());
}
QDateTime from = params.contains("from") ? QDateTime::fromMSecsSinceEpoch(params.value("from").toLongLong() * 1000) : QDateTime();
QDateTime to = params.contains("to") ? QDateTime::fromMSecsSinceEpoch(params.value("to").toLongLong() * 1000) : QDateTime();
QVariantMap returns;
returns.insert("thingPowerLogEntries", pack(m_energyManager->logs()->thingPowerLogs(sampleRate, thingIds, from, to)));
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);
}

View File

@ -0,0 +1,33 @@
#ifndef ENERGYJSONHANDLER_H
#define ENERGYJSONHANDLER_H
#include <QObject>
#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 &params);
Q_INVOKABLE JsonReply* SetRootMeter(const QVariantMap &params);
Q_INVOKABLE JsonReply* GetPowerBalance(const QVariantMap &params);
Q_INVOKABLE JsonReply* GetPowerBalanceLogs(const QVariantMap &params);
Q_INVOKABLE JsonReply* GetThingPowerLogs(const QVariantMap &params);
signals:
void RootMeterChanged(const QVariantMap &params);
void PowerBalanceChanged(const QVariantMap &params);
void PowerBalanceLogEntryAdded(const QVariantMap &params);
void ThingPowerLogEntryAdded(const QVariantMap &params);
private:
EnergyManager *m_energyManager = nullptr;
};
#endif // ENERGYJSONHANDLER_H

876
plugin/energylogger.cpp Normal file
View File

@ -0,0 +1,876 @@
#include "energylogger.h"
#include <nymeasettings.h>
#include <QStandardPaths>
#include <QDir>
#include <QSqlQuery>
#include <QSqlError>
#include <QSqlRecord>
#include <QSettings>
#include <QLoggingCategory>
Q_DECLARE_LOGGING_CATEGORY(dcEnergyExperience)
EnergyLogger::EnergyLogger(QObject *parent) : EnergyLogs(parent)
{
if (!initDB()) {
qCCritical(dcEnergyExperience()) << "Unable to open energy log. Energy logs will not be available.";
return;
}
// Logging configuration
// Note: SampleRate1Min is always sampled as it is the base series for others
// Make sure your base series always has enough samples to build a full sample
// of all series building on it.
// Disk space considerations;
// Each entry takes approx 50 bytes for powerBalance + 60 bytes for thingCurrentPower per thing of disk space
// SQLite adds metadata and overhead of about 5%
// The resulting database size can be estimated with (count being the sum of all numbers below):
// (count * 50 bytes) + (count * things * 60 bytes) + 5%
// 10000 entries, with 5 energy things => ~4MB
// Note: use sqlite3_analyzer to see the approx. size per entry in each table.
m_maxMinuteSamples = 15;
addConfig(SampleRate15Mins, SampleRate1Min, 6720); // 10 weeks
addConfig(SampleRate1Hour, SampleRate15Mins, 1680); // 10 weeks
addConfig(SampleRate3Hours, SampleRate15Mins, 560); // 10 weeks
addConfig(SampleRate1Day, SampleRate1Hour, 1095); // 3 years
addConfig(SampleRate1Week, SampleRate1Day, 168); // 3 years
addConfig(SampleRate1Month, SampleRate1Day, 240); // 20 years
addConfig(SampleRate1Year, SampleRate1Month, 20); // 20 years
// Load last values from thingsPort logs so we have at least one base sample available for sampling, even if a thing might not produce any logs for a while.
foreach (const ThingId &thingId, loggedThings()) {
m_thingsPowerLiveLogs[thingId].append(latestLogEntry(SampleRate1Min, thingId));
}
// Start the scheduling
scheduleNextSample(SampleRate1Min);
foreach (SampleRate sampleRate, m_configs.keys()) {
scheduleNextSample(sampleRate);
}
// Now all the data is initialized. We can start with sampling.
// First check if we missed any samplings (e.g. because the system was offline at the time when it should have created a sample)
foreach(SampleRate sampleRate, m_configs.keys()) {
rectifySamples(sampleRate, m_configs.value(sampleRate).baseSampleRate);
}
// And start the sampler timer
connect(&m_sampleTimer, &QTimer::timeout, this, &EnergyLogger::sample);
m_sampleTimer.start(1000);
}
void EnergyLogger::logPowerBalance(double consumption, double production, double acquisition, double storage, double totalConsumption, double totalProduction, double totalAcquisition, double totalReturn)
{
PowerBalanceLogEntry entry(QDateTime::currentDateTime(), consumption, production, acquisition, storage, totalConsumption, totalProduction, totalAcquisition, totalReturn);
// Add everything to livelog, keep that for one day, in memory only
m_balanceLiveLog.prepend(entry);
while (m_balanceLiveLog.count() > 1 && m_balanceLiveLog.last().timestamp().addDays(1) < QDateTime::currentDateTime()) {
qCDebug(dcEnergyExperience) << "Discarding livelog entry from" << m_balanceLiveLog.last().timestamp().toString();
m_balanceLiveLog.removeLast();
}
}
void EnergyLogger::logThingPower(const ThingId &thingId, double currentPower, double totalConsumption, double totalProduction)
{
ThingPowerLogEntry entry(QDateTime::currentDateTime(), thingId, currentPower, totalConsumption, totalProduction);
m_thingsPowerLiveLogs[thingId].prepend(entry);
while (m_thingsPowerLiveLogs[thingId].count() > 1 && m_thingsPowerLiveLogs[thingId].last().timestamp().addDays(1) < QDateTime::currentDateTime()) {
qCDebug(dcEnergyExperience()) << "Discarding thing power livelog entry for thing" << thingId << "from" << m_thingsPowerLiveLogs[thingId].last().timestamp().toString();
m_thingsPowerLiveLogs[thingId].removeLast();
}
}
PowerBalanceLogEntries EnergyLogger::powerBalanceLogs(SampleRate sampleRate, const QDateTime &from, const QDateTime &to) const
{
PowerBalanceLogEntries result;
QSqlQuery query(m_db);
QString queryString = "SELECT * FROM powerBalance WHERE sampleRate = ?";
QVariantList bindValues;
bindValues << sampleRate;
qCDebug(dcEnergyExperience()) << "Fetching logs. Timestamp:" << from << from.isNull();
if (!from.isNull()) {
queryString += " AND timestamp >= ?";
bindValues << from.toMSecsSinceEpoch();
}
if (!to.isNull()) {
queryString += " AND timestamp <= ?";
bindValues << to.toMSecsSinceEpoch();
}
query.prepare(queryString);
foreach (const QVariant &bindValue, bindValues) {
query.addBindValue(bindValue);
}
qCDebug(dcEnergyExperience()) << "Executing" << queryString << bindValues;
query.exec();
if (query.lastError().isValid()) {
qCWarning(dcEnergyExperience()) << "Error fetching power balance logs:" << query.lastError() << query.executedQuery();
return result;
}
while (query.next()) {
// qCDebug(dcEnergyExperience()) << "Adding result";
result.append(queryResultToBalanceLogEntry(query.record()));
}
return result;
}
ThingPowerLogEntries EnergyLogger::thingPowerLogs(SampleRate sampleRate, const QList<ThingId> &thingIds, const QDateTime &from, const QDateTime &to) const
{
ThingPowerLogEntries result;
QSqlQuery query(m_db);
QString queryString = "SELECT * FROM thingPower WHERE sampleRate = ?";
QVariantList bindValues;
bindValues << sampleRate;
qCDebug(dcEnergyExperience()) << "Fetching thing power logs for" << thingIds;
QStringList thingsQuery;
foreach (const ThingId &thingId, thingIds) {
thingsQuery.append("thingId = ?");
bindValues << thingId;
}
if (!thingsQuery.isEmpty()) {
queryString += " AND (" + thingsQuery.join(" OR ") + " )";
}
if (!from.isNull()) {
queryString += " AND timestamp >= ?";
bindValues << from.toMSecsSinceEpoch();
}
if (!to.isNull()) {
queryString += " AND timestamp <= ?";
bindValues << to.toMSecsSinceEpoch();
}
query.prepare(queryString);
foreach (const QVariant &bindValue, bindValues) {
query.addBindValue(bindValue);
}
query.exec();
if (query.lastError().isValid()) {
qCWarning(dcEnergyExperience()) << "Error fetching power balance logs:" << query.lastError() << query.executedQuery();
return result;
}
while (query.next()) {
result.append(ThingPowerLogEntry(
QDateTime::fromMSecsSinceEpoch(query.value("timestamp").toLongLong()),
query.value("thingId").toUuid(),
query.value("currentPower").toDouble(),
query.value("totalConsumption").toDouble(),
query.value("totalProduction").toDouble()));
}
return result;
}
PowerBalanceLogEntry EnergyLogger::latestLogEntry(SampleRate sampleRate)
{
if (sampleRate == SampleRateAny) {
if (m_balanceLiveLog.count() > 0) {
return m_balanceLiveLog.first();
}
}
QSqlQuery query(m_db);
QString queryString = "SELECT MAX(timestamp), consumption, production, acquisition, storage, totalConsumption, totalProduction, totalAcquisition, totalReturn FROM powerBalance";
QVariantList bindValues;
if (sampleRate != SampleRateAny) {
queryString += " WHERE sampleRate = ?";
bindValues.append(sampleRate);
}
queryString += ";";
query.prepare(queryString);
foreach (const QVariant &value, bindValues) {
query.addBindValue(value);
}
query.exec();
if (query.lastError().isValid()) {
qCWarning(dcEnergyExperience()) << "Error obtaining latest log entry from DB:" << query.lastError() << query.executedQuery();
return PowerBalanceLogEntry();
}
if (!query.next()) {
qCDebug(dcEnergyExperience()) << "No power balance log entry in DB for sample rate:" << sampleRate;
return PowerBalanceLogEntry();
}
return queryResultToBalanceLogEntry(query.record());
}
ThingPowerLogEntry EnergyLogger::latestLogEntry(SampleRate sampleRate, const ThingId &thingId)
{
if (sampleRate == SampleRateAny) {
if (m_thingsPowerLiveLogs.value(thingId).count() > 0) {
return m_thingsPowerLiveLogs.value(thingId).first();
}
}
QSqlQuery query(m_db);
query.prepare("SELECT MAX(timestamp), currentPower, totalConsumption, totalProduction from thingPower WHERE sampleRate = ? AND thingId = ?;");
query.addBindValue(sampleRate);
query.addBindValue(thingId);
if (!query.exec()) {
qCWarning(dcEnergyExperience()) << "Error fetching latest thing log entry from DB:" << query.lastError() << query.executedQuery();
return ThingPowerLogEntry();
}
if (!query.next()) {
qCDebug(dcEnergyExperience()) << "No thing power log entry in DB for sample rate:" << sampleRate;
return ThingPowerLogEntry();
}
return queryResultToThingPowerLogEntry(query.record());
}
void EnergyLogger::removeThingLogs(const ThingId &thingId)
{
QSqlQuery query(m_db);
query.prepare("DELETE FROM thingPower WHERE thingId = ?;");
query.addBindValue(thingId);
query.exec();
if (query.lastError().isValid()) {
qCWarning(dcEnergyExperience()) << "Error removing thing energy logs for thing id" << thingId << query.lastError() << query.executedQuery();
}
}
QList<ThingId> EnergyLogger::loggedThings() const
{
QList<ThingId> ret;
QSqlQuery query(m_db);
query.prepare("SELECT DISTINCT thingId FROM thingPower;");
query.exec();
if (query.lastError().isValid()) {
qCWarning(dcEnergyExperience()) << "Failed to load existing things from logs:" << query.lastError();
} else {
while (query.next()) {
ret.append(query.value("thingId").toUuid());
}
}
return ret;
}
void EnergyLogger::cacheThingEntry(const ThingId &thingId, double totalEnergyConsumed, double totalEnergyProduced)
{
QSqlQuery query(m_db);
query.prepare("INSERT OR REPLACE INTO thingCache (thingId, totalEnergyConsumed, totalEnergyProduced) VALUES (?, ?, ?);");
query.addBindValue(thingId);
query.addBindValue(totalEnergyConsumed);
query.addBindValue(totalEnergyProduced);
query.exec();
if (query.lastError().isValid()) {
qCWarning(dcEnergyExperience()) << "Failed to store thing cache entry:" << query.lastError() << query.executedQuery();
}
}
ThingPowerLogEntry EnergyLogger::cachedThingEntry(const ThingId &thingId)
{
QSqlQuery query(m_db);
query.prepare("SELECT * FROM thingCache WHERE thingId = ?;");
query.addBindValue(thingId);
query.exec();
if (query.lastError().isValid()) {
qCWarning(dcEnergyExperience()) << "Failed to retrieve thing cache entry:" << query.lastError() << query.executedQuery();
return ThingPowerLogEntry();
}
if (!query.next()) {
qCDebug(dcEnergyExperience()) << "No cached thing entry for" << thingId;
return ThingPowerLogEntry();
}
return ThingPowerLogEntry(QDateTime(), thingId, 0, query.value("totalEnergyConsumed").toDouble(), query.value("totalEnergyProduced").toDouble());
}
void EnergyLogger::sample()
{
QDateTime now = QDateTime::currentDateTime();
if (now >= m_nextSamples.value(SampleRate1Min)) {
QDateTime sampleEnd = m_nextSamples.value(SampleRate1Min);
QDateTime sampleStart = sampleEnd.addMSecs(-60 * 1000);
qCDebug(dcEnergyExperience()) << "Sampling power balance for 1 min" << sampleEnd.toString();
double medianConsumption = 0;
double medianProduction = 0;
double medianAcquisition = 0;
double medianStorage = 0;
for (int i = 0; i < m_balanceLiveLog.count(); i++) {
const PowerBalanceLogEntry &entry = m_balanceLiveLog.at(i);
QDateTime frameStart = (entry.timestamp() < sampleStart) ? sampleStart : entry.timestamp();
QDateTime frameEnd = i == 0 ? sampleEnd : m_balanceLiveLog.at(i-1).timestamp();
int frameDuration = frameStart.msecsTo(frameEnd);
qCDebug(dcEnergyExperience()) << "Frame" << i << "duration:" << frameDuration << "value:" << entry.consumption() << "start" << frameStart.toString() << "end" << frameEnd.toString();
medianConsumption += entry.consumption() * frameDuration;
medianProduction += entry.production() * frameDuration;
medianAcquisition += entry.acquisition() * frameDuration;
medianStorage += entry.storage() * frameDuration;
if (entry.timestamp() < sampleStart) {
break;
}
}
medianConsumption /= sampleStart.msecsTo(sampleEnd);
medianProduction /= sampleStart.msecsTo(sampleEnd);
medianAcquisition /= sampleStart.msecsTo(sampleEnd);
medianStorage /= sampleStart.msecsTo(sampleEnd);
PowerBalanceLogEntry newest = m_balanceLiveLog.count() > 0 ? m_balanceLiveLog.at(0) : PowerBalanceLogEntry();
double totalConsumption = newest.totalConsumption();
double totalProduction = newest.totalProduction();
double totalAcquisition = newest.totalAcquisition();
double totalReturn = newest.totalReturn();
qCDebug(dcEnergyExperience()) << "Sampled power balance:" << SampleRate1Min << "🔥:" << medianConsumption << "🌞:" << medianProduction << "💵:" << medianAcquisition << "🔋:" << medianStorage << "Totals:" << "🔥:" << totalConsumption << "🌞:" << totalProduction << "💵↓:" << totalAcquisition << "💵↑:" << totalReturn;
insertPowerBalance(sampleEnd, SampleRate1Min, medianConsumption, medianProduction, medianAcquisition, medianStorage, totalConsumption, totalProduction, totalAcquisition, totalReturn);
foreach (const ThingId &thingId, m_thingsPowerLiveLogs.keys()) {
double medianPower = 0;
qCDebug(dcEnergyExperience()) << "Sampling thing power for" << thingId.toString() << SampleRate1Min << sampleEnd.toString();
ThingPowerLogEntries entries = m_thingsPowerLiveLogs.value(thingId);
for (int i = 0; i < entries.count(); i++) {
const ThingPowerLogEntry &entry = entries.at(i);
QDateTime frameStart = (entry.timestamp() < sampleStart) ? sampleStart : entry.timestamp();
QDateTime frameEnd = i == 0 ? sampleEnd : entries.at(i-1).timestamp();
int frameDuration = frameStart.msecsTo(frameEnd);
qCDebug(dcEnergyExperience()) << "Frame" << i << "duration:" << frameDuration << "value:" << entry.currentPower();
medianPower += entry.currentPower() * frameDuration;
if (entry.timestamp() < sampleStart) {
break;
}
}
medianPower /= sampleStart.msecsTo(sampleEnd);
ThingPowerLogEntry newest = entries.count() > 0 ? entries.first() : ThingPowerLogEntry();
double totalConsumption = newest.totalConsumption();
double totalProduction = newest.totalProduction();
qCDebug(dcEnergyExperience()) << "Sampled thing power for" << thingId << SampleRate1Min << "🔥/🌞:" << medianPower << "Totals:" << "🔥:" << totalConsumption << "🌞:" << totalProduction;
insertThingPower(sampleEnd, SampleRate1Min, thingId, medianPower, totalConsumption, totalProduction);
}
}
// First sample all the configs.
foreach (SampleRate sampleRate, m_configs.keys()) {
if (now >= m_nextSamples.value(sampleRate)) {
QDateTime sampleTime = m_nextSamples.value(sampleRate);
SampleRate baseSampleRate = m_configs.value(sampleRate).baseSampleRate;
samplePowerBalance(sampleRate, baseSampleRate, sampleTime);
sampleThingsPower(sampleRate, baseSampleRate, sampleTime);
}
}
// and then trim them
if (now > m_nextSamples.value(SampleRate1Min)) {
QDateTime sampleTime = m_nextSamples.value(SampleRate1Min);
QDateTime oldestTimestamp = sampleTime.addMSecs(-(qint64)m_maxMinuteSamples * 60 * 1000);
trimPowerBalance(SampleRate1Min, oldestTimestamp);
foreach (const ThingId &thingId, m_thingsPowerLiveLogs.keys()) {
trimThingPower(thingId, SampleRate1Min, oldestTimestamp);
}
}
foreach (SampleRate sampleRate, m_configs.keys()) {
if (now >= m_nextSamples.value(sampleRate)) {
QDateTime sampleTime = m_nextSamples.value(sampleRate);
QDateTime oldestTimestamp = sampleTime.addMSecs(-(qint64)m_configs.value(sampleRate).maxSamples * sampleRate * 60 * 1000);
trimPowerBalance(sampleRate, oldestTimestamp);
foreach (const ThingId &thingId, m_thingsPowerLiveLogs.keys()) {
trimThingPower(thingId, sampleRate, oldestTimestamp);
}
}
}
// Lastly we reschedule the next sample for each config
// Note: keep this at the end as the previous stuff uses the schedule to work
if (now > m_nextSamples.value(SampleRate1Min)) {
scheduleNextSample(SampleRate1Min);
}
foreach (SampleRate sampleRate, m_configs.keys()) {
if (now >= m_nextSamples.value(sampleRate)) {
scheduleNextSample(sampleRate);
}
}
}
bool EnergyLogger::initDB()
{
m_db.close();
m_db = QSqlDatabase::addDatabase("QSQLITE", "energylogs");
QDir path = QDir(NymeaSettings::storagePath());
if (!path.exists()) {
path.mkpath(path.path());
}
m_db.setDatabaseName(path.filePath("energylogs.sqlite"));
bool opened = m_db.open();
if (!opened) {
qCWarning(dcEnergyExperience()) << "Cannot open energy log DB at" << m_db.databaseName() << m_db.lastError();
return false;
}
if (!m_db.tables().contains("metadata")) {
qCDebug(dcEnergyExperience()) << "No \metadata\" table in database. Creating it.";
m_db.exec("CREATE TABLE metadata (version INT);");
m_db.exec("INSERT INTO metadata (version) VALUES (1);");
if (m_db.lastError().isValid()) {
qCWarning(dcEnergyExperience()) << "Error creating metadata table in energy log database. Driver error:" << m_db.lastError().driverText() << "Database error:" << m_db.lastError().databaseText();
return false;
}
}
if (!m_db.tables().contains("powerBalance")) {
qCDebug(dcEnergyExperience()) << "No \"powerBalance\" table in database. Creating it.";
m_db.exec("CREATE TABLE powerBalance "
"("
"timestamp BIGINT,"
"sampleRate INT,"
"consumption FLOAT,"
"production FLOAT,"
"acquisition FLOAT,"
"storage FLOAT,"
"totalConsumption FLOAT,"
"totalProduction FLOAT,"
"totalAcquisition FLOAT,"
"totalReturn FLOAT"
");");
if (m_db.lastError().isValid()) {
qCWarning(dcEnergyExperience()) << "Error creating powerBalance table in energy log database. Driver error:" << m_db.lastError().driverText() << "Database error:" << m_db.lastError().databaseText();
return false;
}
}
if (!m_db.tables().contains("thingPower")) {
qCDebug(dcEnergyExperience()) << "No \"thingPower\" table in database. Creating it.";
m_db.exec("CREATE TABLE thingPower "
"("
"timestamp BIGINT,"
"sampleRate INT,"
"thingId VARCHAR(38),"
"currentPower FLOAT,"
"totalConsumption FLOAT,"
"totalProduction FLOAT"
");");
if (m_db.lastError().isValid()) {
qCWarning(dcEnergyExperience()) << "Error creating thingPower table in energy log database. Driver error:" << m_db.lastError().driverText() << "Database error:" << m_db.lastError().databaseText();
return false;
}
}
if (!m_db.tables().contains("thingCache")) {
qCDebug(dcEnergyExperience()) << "No \"thingCache\" table in database. Creating it.";
m_db.exec("CREATE TABLE thingCache "
"("
"thingId VARCHAR(38) PRIMARY KEY,"
"totalEnergyConsumed FLOAT,"
"totalEnergyProduced FLOAT"
");");
if (m_db.lastError().isValid()) {
qCWarning(dcEnergyExperience()) << "Error creating thingCache table in energy log database. Driver error:" << m_db.lastError().driverText() << "Database error:" << m_db.lastError().databaseText();
return false;
}
}
qCDebug(dcEnergyExperience()) << "Initialized logging DB successfully." << m_db.databaseName();
return true;
}
void EnergyLogger::addConfig(SampleRate sampleRate, SampleRate baseSampleRate, int maxSamples)
{
SampleConfig config;
config.baseSampleRate = baseSampleRate;
config.maxSamples = maxSamples;
m_configs.insert(sampleRate, config);
}
QDateTime EnergyLogger::getOldestPowerBalanceSampleTimestamp(SampleRate sampleRate)
{
QSqlQuery query(m_db);
query.prepare("SELECT MIN(timestamp) AS oldestTimestamp FROM powerBalance WHERE sampleRate = ?;");
query.addBindValue(sampleRate);
query.exec();
if (query.next() && !query.value("oldestTimestamp").isNull()) {
return QDateTime::fromMSecsSinceEpoch(query.value("oldestTimestamp").toLongLong());
}
return QDateTime();
}
QDateTime EnergyLogger::getNewestPowerBalanceSampleTimestamp(SampleRate sampleRate)
{
QSqlQuery query(m_db);
query.prepare("SELECT MAX(timestamp) AS latestTimestamp FROM powerBalance WHERE sampleRate = ?;");
query.addBindValue(sampleRate);
query.exec();
if (query.next() && !query.value("latestTimestamp").isNull()) {
return QDateTime::fromMSecsSinceEpoch(query.value("latestTimestamp").toLongLong());
}
return QDateTime();
}
QDateTime EnergyLogger::getOldestThingPowerSampleTimestamp(const ThingId &thingId, SampleRate sampleRate)
{
QSqlQuery query(m_db);
query.prepare("SELECT MIN(timestamp) AS oldestTimestamp FROM thingPower WHERE thingId = ? AND sampleRate = ?;");
query.addBindValue(thingId);
query.addBindValue(sampleRate);
query.exec();
if (query.next() && !query.value("oldestTimestamp").isNull()) {
return QDateTime::fromMSecsSinceEpoch(query.value("oldestTimestamp").toLongLong());
}
return QDateTime();
}
QDateTime EnergyLogger::getNewestThingPowerSampleTimestamp(const ThingId &thingId, SampleRate sampleRate)
{
QSqlQuery query(m_db);
query.prepare("SELECT MAX(timestamp) AS newestTimestamp FROM thingPower WHERE thingId = ? AND sampleRate = ?;");
query.addBindValue(thingId);
query.addBindValue(sampleRate);
query.exec();
if (query.next() && !query.value("newestTimestamp").isNull()) {
return QDateTime::fromMSecsSinceEpoch(query.value("newestTimestamp").toLongLong());
}
return QDateTime();
}
void EnergyLogger::scheduleNextSample(SampleRate sampleRate)
{
QDateTime next = nextSampleTimestamp(sampleRate, QDateTime::currentDateTime());
m_nextSamples.insert(sampleRate, next);
qCDebug(dcEnergyExperience()) << "Next sample for" << sampleRate << "scheduled at" << next.toString();
}
void EnergyLogger::rectifySamples(SampleRate sampleRate, SampleRate baseSampleRate)
{
// Normally we'd need to find the newest available sample of a series and catch up from there.
// However, it could happen a series does not have any samples at all yet. For example if we're logging since january,
// and at new years the system was off, we missed the new years yearly sample and don't have any earlier. For those cases
// we need to start resampling from the oldest timestamp we find in the DB for the base sampleRate.
QDateTime oldestBaseSample = getOldestPowerBalanceSampleTimestamp(baseSampleRate);
QDateTime newestSample = getNewestPowerBalanceSampleTimestamp(sampleRate);
qCDebug(dcEnergyExperience()) << "Checking for missing samples for" << sampleRate;
// qCDebug(dcEnergyExperience()) << "Newest sample:" << newestSample.toString() << "Oldest base sample:" << oldestBaseSample.toString();
if (newestSample.isNull()) {
// qCDebug(dcEnergyExperience()) << "No sample at all so far. Using base as starting point.";
newestSample = oldestBaseSample;
}
// qCDebug(dcEnergyExperience()) << "next sample after last in series:" << nextSampleTimestamp(sampleRate, newestSample).toString();
// qCDebug(dcEnergyExperience()) << "next scheduled sample:" << m_nextSamples.value(sampleRate).toString();
while (!newestSample.isNull() && nextSampleTimestamp(sampleRate, newestSample) < m_nextSamples[sampleRate]) {
QDateTime nextSample = nextSampleTimestamp(sampleRate, newestSample.addMSecs(1000));
// qCDebug(dcEnergyExperience()) << "Rectifying missed sample for" << sampleRate << "from" << nextSample.toString();
samplePowerBalance(sampleRate, baseSampleRate, nextSample);
newestSample = nextSample;
}
foreach (const ThingId &thingId, m_thingsPowerLiveLogs.keys()) {
QDateTime oldestBaseSample = getOldestThingPowerSampleTimestamp(thingId, baseSampleRate);
QDateTime newestSample = getNewestThingPowerSampleTimestamp(thingId, sampleRate);
// qCDebug(dcEnergyExperience()) << "T Checking for missing samples for" << sampleRate;
// qCDebug(dcEnergyExperience()) << "T Newest sample:" << newestSample.toString() << "Oldest base sample:" << oldestBaseSample.toString();
if (newestSample.isNull()) {
// qCDebug(dcEnergyExperience()) << "T No sample at all so far. Using base as starting point.";
newestSample = oldestBaseSample;
}
// qCDebug(dcEnergyExperience()) << "T next sample after last in series:" << nextSampleTimestamp(sampleRate, newestSample).toString();
// qCDebug(dcEnergyExperience()) << "T next scheduled sample:" << m_nextSamples.value(sampleRate).toString();
while (!newestSample.isNull() && nextSampleTimestamp(sampleRate, newestSample) < m_nextSamples[sampleRate]) {
QDateTime nextSample = nextSampleTimestamp(sampleRate, newestSample.addMSecs(1000));
// qCDebug(dcEnergyExperience()) << "T Rectifying missed sample for" << sampleRate << "from" << nextSample.toString();
sampleThingPower(thingId, sampleRate, baseSampleRate, nextSample);
newestSample = nextSample;
}
}
}
QDateTime EnergyLogger::nextSampleTimestamp(SampleRate sampleRate, const QDateTime &dateTime)
{
QTime time = dateTime.time();
QDate date = dateTime.date();
QDateTime next;
switch (sampleRate) {
case SampleRateAny:
qCWarning(dcEnergyExperience()) << "Cannot calculate next sample timestamp without a sample rate";
return QDateTime();
case SampleRate1Min:
time.setHMS(time.hour(), time.minute(), 0);
next = QDateTime(date, time).addMSecs(60 * 1000);
break;
case SampleRate15Mins:
time.setHMS(time.hour(), time.minute() - (time.minute() % 15), 0);
next = QDateTime(date, time).addMSecs(15 * 60 * 1000);
break;
case SampleRate1Hour:
time.setHMS(time.hour(), 0, 0);
next = QDateTime(date, time).addMSecs(60 * 60 * 1000);
break;
case SampleRate3Hours:
time.setHMS(time.hour() - (time.hour() % 3), 0, 0);
next = QDateTime(date, time).addMSecs(3 * 60 * 60 * 1000);
break;
case SampleRate1Day:
next = QDateTime(date, QTime()).addDays(1);
break;
case SampleRate1Week:
date = date.addDays(-date.dayOfWeek() + 1);
next = QDateTime(date, QTime()).addDays(7);
break;
case SampleRate1Month:
date = date.addDays(-date.day() + 1);
next = QDateTime(date, QTime()).addMonths(1);
break;
case SampleRate1Year:
date.setDate(date.year(), 1, 1);
next = QDateTime(date, QTime()).addYears(1);
break;
}
return next;
}
bool EnergyLogger::samplePowerBalance(SampleRate sampleRate, SampleRate baseSampleRate, const QDateTime &sampleEnd)
{
QDateTime sampleStart = sampleEnd.addMSecs(-sampleRate * 60 * 1000);
qCDebug(dcEnergyExperience()) << "Sampling power balance" << sampleRate << "from" << sampleStart << "to" << sampleEnd;
double medianConsumption = 0;
double medianProduction = 0;
double medianAcquisition = 0;
double medianStorage = 0;
double totalConsumption = 0;
double totalProduction = 0;
double totalAcquisition = 0;
double totalReturn = 0;
QSqlQuery query(m_db);
query.prepare("SELECT * FROM powerBalance WHERE sampleRate = ? AND timestamp > ? AND timestamp <= ?;");
query.addBindValue(baseSampleRate);
query.addBindValue(sampleStart.toMSecsSinceEpoch());
query.addBindValue(sampleEnd.toMSecsSinceEpoch());
query.exec();
if (query.lastError().isValid()) {
qCWarning(dcEnergyExperience()) << "Error fetching power balance samples for" << baseSampleRate << "from" << sampleStart.toString() << "to" << sampleEnd.toString();
qCWarning(dcEnergyExperience()) << "SQL error was:" << query.lastError() << "executed query:" << query.executedQuery();
return false;
}
int resultCount = 0;
while (query.next()) {
resultCount++;
qCDebug(dcEnergyExperience()) << "Frame:" << QDateTime::fromMSecsSinceEpoch(query.value("timestamp").toLongLong()).toString() << query.value("consumption").toDouble() << query.value("production").toDouble() << query.value("acquisition").toDouble() << query.value("storage").toDouble() << query.value("totalConsumption").toDouble() << query.value("totalProduction").toDouble() << query.value("totalAcquisition").toDouble() << query.value("totalReturn").toDouble();
medianConsumption += query.value("consumption").toDouble();
medianProduction += query.value("production").toDouble();
medianAcquisition += query.value("acquisition").toDouble();
medianStorage += query.value("storage").toDouble();
totalConsumption = query.value("totalConsumption").toDouble();
totalProduction = query.value("totalProduction").toDouble();
totalAcquisition = query.value("totalAcquisition").toDouble();
totalReturn = query.value("totalReturn").toDouble();
}
if (resultCount > 0) {
medianConsumption = medianConsumption * baseSampleRate / sampleRate;
medianProduction = medianProduction * baseSampleRate / sampleRate;
medianAcquisition = medianAcquisition * baseSampleRate / sampleRate;
medianStorage = medianStorage * baseSampleRate / sampleRate;
} else {
// If there are no base samples for the given time frame at all, let's try to find the last existing one in the base
// to at least copy the totals from where we left off.
query = QSqlQuery(m_db);
query.prepare("SELECT MAX(timestamp), consumption, production, acquisition, storage, totalConsumption, totalProduction, totalAcquisition, totalReturn FROM powerBalance WHERE sampleRate = ?;");
query.addBindValue(baseSampleRate);
query.exec();
if (query.lastError().isValid()) {
qCWarning(dcEnergyExperience()) << "Error fetching newest power balance sample for" << baseSampleRate;
qCWarning(dcEnergyExperience()) << "SQL error was:" << query.lastError() << "executed query:" << query.executedQuery();
return false;
}
if (query.next()) {
totalConsumption = query.value("totalConsumption").toDouble();
totalProduction = query.value("totalProduction").toDouble();
totalAcquisition = query.value("totalAcquisition").toDouble();
totalReturn = query.value("totalReturn").toDouble();
}
}
qCDebug(dcEnergyExperience()) << "Sampled:" << "🔥:" << medianConsumption << "🌞:" << medianProduction << "💵:" << medianAcquisition << "🔋:" << medianStorage << "Totals:" << "🔥:" << totalConsumption << "🌞:" << totalProduction << "💵↓:" << totalAcquisition << "💵↑:" << totalReturn;
return insertPowerBalance(sampleEnd, sampleRate, medianConsumption, medianProduction, medianAcquisition, medianStorage, totalConsumption, totalProduction, totalAcquisition, totalReturn);
}
bool EnergyLogger::insertPowerBalance(const QDateTime &timestamp, SampleRate sampleRate, double consumption, double production, double acquisition, double storage, double totalConsumption, double totalProduction, double totalAcquisition, double totalReturn)
{
QSqlQuery query = QSqlQuery(m_db);
query.prepare("INSERT INTO powerBalance (timestamp, sampleRate, consumption, production, acquisition, storage, totalConsumption, totalProduction, totalAcquisition, totalReturn) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?);");
query.addBindValue(timestamp.toMSecsSinceEpoch());
query.addBindValue(sampleRate);
query.addBindValue(consumption);
query.addBindValue(production);
query.addBindValue(acquisition);
query.addBindValue(storage);
query.addBindValue(totalConsumption);
query.addBindValue(totalProduction);
query.addBindValue(totalAcquisition);
query.addBindValue(totalReturn);
query.exec();
if (query.lastError().isValid()) {
qCWarning(dcEnergyExperience()) << "Error logging consumption sample:" << query.lastError() << query.executedQuery();
return false;
}
emit powerBalanceEntryAdded(sampleRate, PowerBalanceLogEntry(timestamp, consumption, production, acquisition, storage, totalConsumption, totalProduction, totalAcquisition, totalReturn));
return true;
}
bool EnergyLogger::sampleThingsPower(SampleRate sampleRate, SampleRate baseSampleRate, const QDateTime &sampleEnd)
{
bool ret = true;
foreach (const ThingId &thingId, m_thingsPowerLiveLogs.keys()) {
ret &= sampleThingPower(thingId, sampleRate, baseSampleRate, sampleEnd);
}
return ret;
}
bool EnergyLogger::sampleThingPower(const ThingId &thingId, SampleRate sampleRate, SampleRate baseSampleRate, const QDateTime &sampleEnd)
{
QDateTime sampleStart = sampleEnd.addMSecs(-sampleRate * 60 * 1000);
qCDebug(dcEnergyExperience()) << "Sampling thing power for" << thingId.toString() << sampleRate << "from" << sampleStart.toString() << "to" << sampleEnd.toString();
double medianCurrentPower = 0;
double totalConsumption = 0;
double totalProduction = 0;
QSqlQuery query(m_db);
query.prepare("SELECT * FROM thingPower WHERE thingId = ? AND sampleRate = ? AND timestamp > ? AND timestamp <= ?;");
query.addBindValue(thingId);
query.addBindValue(baseSampleRate);
query.addBindValue(sampleStart.toMSecsSinceEpoch());
query.addBindValue(sampleEnd.toMSecsSinceEpoch());
query.exec();
if (query.lastError().isValid()) {
qCWarning(dcEnergyExperience()) << "Error fetching thing power samples for" << baseSampleRate << "from" << sampleStart.toString() << "to" << sampleEnd.toString();
qCWarning(dcEnergyExperience()) << "SQL error was:" << query.lastError() << "executed query:" << query.executedQuery();
return false;
}
qCDebug(dcEnergyExperience()) << "Query:" << query.executedQuery();
qCDebug(dcEnergyExperience()) << "Results:" << query.size();
int resultCount = 0;
while (query.next()) {
resultCount++;
qCDebug(dcEnergyExperience()) << "Frame:" << query.value("currentPower").toDouble() << QDateTime::fromMSecsSinceEpoch(query.value("timestamp").toLongLong()).toString();
medianCurrentPower += query.value("currentPower").toDouble();
totalConsumption = query.value("totalConsumption").toDouble();
totalProduction = query.value("totalProduction").toDouble();
}
if (resultCount > 0) {
medianCurrentPower = medianCurrentPower * baseSampleRate / sampleRate;
} else {
// If there are no base samples for the given time frame at all, let's try to find the last existing one in the base
// to at least copy the totals from where we left off.
query = QSqlQuery(m_db);
query.prepare("SELECT MAX(timestamp), currentPower, totalConsumption, totalProduction FROM thingPower WHERE thingId = ? AND sampleRate = ?;");
query.addBindValue(thingId);
query.addBindValue(baseSampleRate);
query.exec();
if (query.lastError().isValid()) {
qCWarning(dcEnergyExperience()) << "Error fetching newest thing power sample for" << thingId.toString() << baseSampleRate;
qCWarning(dcEnergyExperience()) << "SQL error was:" << query.lastError() << "executed query:" << query.executedQuery();
return false;
}
if (query.next()) {
totalConsumption = query.value("totalConsumption").toDouble();
totalProduction = query.value("totalProduction").toDouble();
}
}
qCDebug(dcEnergyExperience()) << "Sampled:" << thingId.toString() << sampleRate << "median currentPower:" << medianCurrentPower << "total consumption:" << totalConsumption << "total production:" << totalProduction;
return insertThingPower(sampleEnd, sampleRate, thingId, medianCurrentPower, totalConsumption, totalProduction);
}
bool EnergyLogger::insertThingPower(const QDateTime &timestamp, SampleRate sampleRate, const ThingId &thingId, double currentPower, double totalConsumption, double totalProduction)
{
QSqlQuery query = QSqlQuery(m_db);
query.prepare("INSERT INTO thingPower (timestamp, sampleRate, thingId, currentPower, totalConsumption, totalProduction) values (?, ?, ?, ?, ?, ?);");
query.addBindValue(timestamp.toMSecsSinceEpoch());
query.addBindValue(sampleRate);
query.addBindValue(thingId);
query.addBindValue(currentPower);
query.addBindValue(totalConsumption);
query.addBindValue(totalProduction);
query.exec();
if (query.lastError().isValid()) {
qCWarning(dcEnergyExperience()) << "Error logging thing power sample:" << query.lastError() << query.executedQuery();
return false;
}
emit thingPowerEntryAdded(sampleRate, ThingPowerLogEntry(timestamp, thingId, currentPower, totalConsumption, totalProduction));
return true;
}
void EnergyLogger::trimPowerBalance(SampleRate sampleRate, const QDateTime &beforeTime)
{
QSqlQuery query(m_db);
query.prepare("DELETE FROM powerBalance WHERE sampleRate = ? AND timestamp < ?;");
query.addBindValue(sampleRate);
query.addBindValue(beforeTime.toMSecsSinceEpoch());
query.exec();
if (query.numRowsAffected() > 0) {
qCDebug(dcEnergyExperience()).nospace() << "Trimmed " << query.numRowsAffected() << " from power balance series: " << sampleRate << " (Older than: " << beforeTime.toString() << ")";
}
}
void EnergyLogger::trimThingPower(const ThingId &thingId, SampleRate sampleRate, const QDateTime &beforeTime)
{
QSqlQuery query(m_db);
query.prepare("DELETE FROM thingPower WHERE thingId = ? AND sampleRate = ? AND timestamp < ?;");
query.addBindValue(thingId);
query.addBindValue(sampleRate);
query.addBindValue(beforeTime.toMSecsSinceEpoch());
query.exec();
if (query.numRowsAffected() > 0) {
qCDebug(dcEnergyExperience()).nospace() << "Trimmed " << query.numRowsAffected() << " from thing power series for: " << thingId << sampleRate << " (Older than: " << beforeTime.toString() << ")";
}
}
PowerBalanceLogEntry EnergyLogger::queryResultToBalanceLogEntry(const QSqlRecord &record) const
{
return PowerBalanceLogEntry(QDateTime::fromMSecsSinceEpoch(record.value("timestamp").toLongLong()),
record.value("consumption").toDouble(),
record.value("production").toDouble(),
record.value("acquisition").toDouble(),
record.value("storage").toDouble(),
record.value("totalConsumption").toDouble(),
record.value("totalProduction").toDouble(),
record.value("totalAcquisition").toDouble(),
record.value("totalReturn").toDouble());
}
ThingPowerLogEntry EnergyLogger::queryResultToThingPowerLogEntry(const QSqlRecord &record) const
{
return ThingPowerLogEntry(QDateTime::fromMSecsSinceEpoch(record.value("timestamp").toULongLong()),
record.value("thingId").toUuid(),
record.value("currentPower").toDouble(),
record.value("totalConsumption").toDouble(),
record.value("totalProduction").toDouble());
}

84
plugin/energylogger.h Normal file
View File

@ -0,0 +1,84 @@
#ifndef ENERGYLOGGER_H
#define ENERGYLOGGER_H
#include "energylogs.h"
#include <typeutils.h>
#include <QObject>
#include <QDateTime>
#include <QSqlDatabase>
#include <QSqlResult>
#include <QTimer>
#include <QMap>
class EnergyLogger : public EnergyLogs
{
Q_OBJECT
public:
explicit EnergyLogger(QObject *parent = nullptr);
void logPowerBalance(double consumption, double production, double acquisition, double storage, 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<ThingId> &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<ThingId> loggedThings() const;
// For internal use, the energymanager needs to cache some values to track things total values
// This is really only here to have a single storage and not keep a separate cache file. Shouldn't be used for anything else
// Note that the returned ThingPowerLogEntry will be incomplete. It won't have a timestamp nor a currentPower value!
void cacheThingEntry(const ThingId &thingId, double totalEnergyConsumed, double totalEnergyProduced);
ThingPowerLogEntry cachedThingEntry(const ThingId &thingId);
private slots:
void sample();
private:
bool initDB();
void addConfig(SampleRate sampleRate, SampleRate baseSampleRate, int maxSamples);
QDateTime getOldestPowerBalanceSampleTimestamp(SampleRate sampleRate);
QDateTime getNewestPowerBalanceSampleTimestamp(SampleRate sampleRate);
QDateTime getOldestThingPowerSampleTimestamp(const ThingId &thingId, SampleRate sampleRate);
QDateTime getNewestThingPowerSampleTimestamp(const ThingId &thingId, SampleRate sampleRate);
QDateTime nextSampleTimestamp(SampleRate sampleRate, const QDateTime &dateTime);
void scheduleNextSample(SampleRate sampleRate);
void rectifySamples(SampleRate sampleRate, EnergyLogger::SampleRate baseSampleRate);
bool samplePowerBalance(SampleRate sampleRate, SampleRate baseSampleRate, const QDateTime &sampleEnd);
bool insertPowerBalance(const QDateTime &timestamp, 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 &timestamp, SampleRate sampleRate, const ThingId &thingId, double currentPower, double totalConsumption, double totalProduction);
void trimPowerBalance(SampleRate sampleRate, const QDateTime &beforeTime);
void trimThingPower(const ThingId &thingId, SampleRate sampleRate, const QDateTime &beforeTime);
PowerBalanceLogEntry queryResultToBalanceLogEntry(const QSqlRecord &record) const;
ThingPowerLogEntry queryResultToThingPowerLogEntry(const QSqlRecord &record) const;
private:
struct SampleConfig {
SampleRate baseSampleRate;
int maxSamples = 0;
};
PowerBalanceLogEntries m_balanceLiveLog;
QHash<ThingId, ThingPowerLogEntries> m_thingsPowerLiveLogs;
QTimer m_sampleTimer;
QHash<SampleRate, QDateTime> m_nextSamples;
QSqlDatabase m_db;
int m_maxMinuteSamples = 0;
QMap<SampleRate, SampleConfig> m_configs;
};
#endif // ENERGYLOGGER_H

View File

@ -0,0 +1,319 @@
#include "energymanagerimpl.h"
#include "energylogger.h"
#include <nymeasettings.h>
#include <qmath.h>
Q_DECLARE_LOGGING_CATEGORY(dcEnergyExperience)
EnergyManagerImpl::EnergyManagerImpl(ThingManager *thingManager, QObject *parent):
EnergyManager(parent),
m_thingManager(thingManager),
m_logger(new EnergyLogger(this))
{
// Most of the time we get a bunch of state changes (currentPower, totals, for inverter, battery, rootmeter)
// at the same time if they're implemented by the same plugin.
// In order to decrease some load on the system, we'll wait for the event loop pass to finish until we actually
// update to accumulate those changes and calculate the change in one go.
m_balanceUpdateTimer.setInterval(0);
m_balanceUpdateTimer.setSingleShot(true);
connect(&m_balanceUpdateTimer, &QTimer::timeout, this, &EnergyManagerImpl::updatePowerBalance);
QSettings settings(NymeaSettings::settingsPath() + "/energy.conf", QSettings::IniFormat);
ThingId rootMeterThingId = settings.value("rootMeterThingId").toUuid();
EnergyManagerImpl::setRootMeter(rootMeterThingId);
qCDebug(dcEnergyExperience()) << "Loaded root meter" << rootMeterThingId;
PowerBalanceLogEntry latestEntry = m_logger->latestLogEntry(EnergyLogs::SampleRateAny);
m_totalConsumption = latestEntry.totalConsumption();
m_totalProduction = latestEntry.totalProduction();
m_totalAcquisition = latestEntry.totalAcquisition();
m_totalReturn = latestEntry.totalReturn();
qCDebug(dcEnergyExperience()) << "Loaded power balance totals. Consumption:" << m_totalConsumption << "Production:" << m_totalProduction << "Acquisition:" << m_totalAcquisition << "Return:" << m_totalReturn;
foreach (Thing *thing, m_thingManager->configuredThings()) {
watchThing(thing);
}
connect(thingManager, &ThingManager::thingAdded, this, &EnergyManagerImpl::watchThing);
connect(thingManager, &ThingManager::thingRemoved, this, &EnergyManagerImpl::unwatchThing);
// Housekeeping on the logger
foreach (const ThingId &thingId, m_logger->loggedThings()) {
if (!m_thingManager->findConfiguredThing(thingId)) {
qCDebug(dcEnergyExperience()) << "Clearing thing logs for unknown thing id" << thingId << "from energy logs.";
m_logger->removeThingLogs(thingId);
}
}
}
Thing *EnergyManagerImpl::rootMeter() const
{
return m_rootMeter;
}
EnergyManager::EnergyError EnergyManagerImpl::setRootMeter(const ThingId &rootMeterId)
{
Thing *rootMeter = m_thingManager->findConfiguredThing(rootMeterId);
if (!rootMeter || !rootMeter->thingClass().interfaces().contains("energymeter")) {
return EnergyErrorInvalidParameter;
}
if (m_rootMeter != rootMeter) {
qCDebug(dcEnergyExperience()) << "Setting root meter to" << rootMeter->name();
m_rootMeter = rootMeter;
QSettings settings(NymeaSettings::settingsPath() + "/energy.conf", QSettings::IniFormat);
settings.setValue("rootMeterThingId", rootMeter->id());
emit rootMeterChanged();
}
return EnergyErrorNoError;
}
double EnergyManagerImpl::currentPowerConsumption() const
{
return m_currentPowerConsumption;
}
double EnergyManagerImpl::currentPowerProduction() const
{
return m_currentPowerProduction;
}
double EnergyManagerImpl::currentPowerAcquisition() const
{
return m_currentPowerAcquisition;
}
double EnergyManagerImpl::currentPowerStorage() const
{
return m_currentPowerStorage;
}
double EnergyManagerImpl::totalConsumption() const
{
return m_totalConsumption;
}
double EnergyManagerImpl::totalProduction() const
{
return m_totalProduction;
}
double EnergyManagerImpl::totalAcquisition() const
{
return m_totalAcquisition;
}
double EnergyManagerImpl::totalReturn() const
{
return m_totalReturn;
}
EnergyLogs *EnergyManagerImpl::logs() const
{
return m_logger;
}
void EnergyManagerImpl::watchThing(Thing *thing)
{
// If we don't have a root meter yet, we'll be auto-setting the first energymeter that appears.
// It may be changed by the user through an API call later.
if (!m_rootMeter && thing->thingClass().interfaces().contains("energymeter")) {
setRootMeter(thing->id());
}
qCDebug(dcEnergyExperience()) << "Watching thing:" << thing->name();
// React on things that require us updating the power balance
if (thing->thingClass().interfaces().contains("energymeter")
|| thing->thingClass().interfaces().contains("smartmeterproducer")
|| thing->thingClass().interfaces().contains("energystorage")) {
connect(thing, &Thing::stateValueChanged, this, [=](const StateTypeId &stateTypeId){
if (thing->thingClass().getStateType(stateTypeId).name() == "currentPower") {
m_balanceUpdateTimer.start();
}
});
}
// React on things that need to be logged
if (thing->thingClass().interfaces().contains("energymeter")
|| thing->thingClass().interfaces().contains("smartmeterconsumer")
|| thing->thingClass().interfaces().contains("smartmeterproducer")
|| thing->thingClass().interfaces().contains("energystorage")) {
// Initialize caches used to calculate diffs
ThingPowerLogEntry entry = m_logger->latestLogEntry(EnergyLogs::SampleRateAny, {thing->id()});
ThingPowerLogEntry stateEntry = m_logger->cachedThingEntry(thing->id());
m_powerBalanceTotalEnergyConsumedCache[thing] = stateEntry.totalConsumption();
m_powerBalanceTotalEnergyProducedCache[thing] = stateEntry.totalProduction();
m_thingsTotalEnergyConsumedCache[thing] = qMakePair<double, double>(stateEntry.totalConsumption(), entry.totalConsumption());
m_thingsTotalEnergyProducedCache[thing] = qMakePair<double, double>(stateEntry.totalProduction(), entry.totalProduction());
qCDebug(dcEnergyExperience()) << "Loaded thing power totals for" << thing->name() << "Consumption:" << entry.totalConsumption() << "Production:" << entry.totalProduction() << "Last thing state consumption:" << stateEntry.totalConsumption() << "production:" << stateEntry.totalProduction();
connect(thing, &Thing::stateValueChanged, this, [=](const StateTypeId &stateTypeId, const QVariant &/*value*/){
if (QStringList({"currentPower", "totalEnergyConsumed", "totalEnergyProduced"}).contains(thing->thingClass().getStateType(stateTypeId).name())) {
// We'll be keeping our own counters, starting from 0 at the time they're added to nymea and increasing with the things counters.
// This way we'll have proper logs even if the thing counter is reset (some things may reset their counter on power loss, factory reset etc)
// and also won't start with huge values if the thing has been counting for a while and only added to nymea later on
// Consumption
double oldThingConsumptionState = m_thingsTotalEnergyConsumedCache.value(thing).first;
double oldThingConsumptionInternal = m_thingsTotalEnergyConsumedCache.value(thing).second;
double newThingConsumptionState = thing->stateValue("totalEnergyConsumed").toDouble();
// For the very first cycle (oldConsumption is 0) we'll sync up on the meter, without actually adding it to our diff
if (oldThingConsumptionState == 0 && newThingConsumptionState != 0) {
qInfo(dcEnergyExperience()) << "Don't have a consumption counter for" << thing->name() << "Synching internal counters to initial value:" << newThingConsumptionState;
oldThingConsumptionState = newThingConsumptionState;
}
// If the thing's meter has been reset in the meantime (newConsumption < oldConsumption) we'll sync down, taking the whole diff from 0 to new value
if (newThingConsumptionState < oldThingConsumptionState) {
qCInfo(dcEnergyExperience()) << "Thing meter for" << thing->name() << "seems to have been reset. Re-synching internal consumption counter.";
oldThingConsumptionState = newThingConsumptionState;
}
double consumptionDiff = newThingConsumptionState - oldThingConsumptionState;
double newThingConsumptionInternal = oldThingConsumptionInternal + consumptionDiff;
m_thingsTotalEnergyConsumedCache[thing] = qMakePair<double, double>(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<double, double>(newThingProductionState, newThingProductionInternal);
// Write to log
qCDebug(dcEnergyExperience()) << "Logging thing" << thing->name() << "total consumption:" << newThingConsumptionInternal << "production:" << newThingProductionInternal;
m_logger->logThingPower(thing->id(), thing->state("currentPower").value().toDouble(), newThingConsumptionInternal, newThingProductionInternal);
// Cache the thing state values in case nymea is restarted
m_logger->cacheThingEntry(thing->id(), newThingConsumptionState, newThingProductionState);
}
});
}
}
void EnergyManagerImpl::unwatchThing(const ThingId &thingId)
{
if (m_rootMeter && m_rootMeter->id() == thingId) {
m_rootMeter = nullptr;
emit rootMeterChanged();
}
m_logger->removeThingLogs(thingId);
}
void EnergyManagerImpl::updatePowerBalance()
{
double currentPowerAcquisition = 0;
if (m_rootMeter) {
currentPowerAcquisition = m_rootMeter->stateValue("currentPower").toDouble();
double oldAcquisition = m_powerBalanceTotalEnergyConsumedCache.value(m_rootMeter);
double newAcquisition = m_rootMeter->stateValue("totalEnergyConsumed").toDouble();
// For the very first cycle (oldAcquisition is 0) we'll sync up on the meter values without actually adding them to our balance.
if (oldAcquisition == 0) {
oldAcquisition = newAcquisition;
}
// If the root meter has been reset in the meantime (newConsumption < oldConsumption) we'll sync down, taking the whole diff from 0 to new value
if (newAcquisition < oldAcquisition) {
qCInfo(dcEnergyExperience()) << "Root meter seems to have been reset. Re-synching internal consumption counter.";
oldAcquisition = newAcquisition;
}
qCDebug(dcEnergyExperience()) << "Root meter total consumption: Previous value:" << oldAcquisition << "New value:" << newAcquisition << "Diff:" << (newAcquisition -oldAcquisition);
m_totalAcquisition += newAcquisition - oldAcquisition;
m_powerBalanceTotalEnergyConsumedCache[m_rootMeter] = newAcquisition;
double oldReturn = m_powerBalanceTotalEnergyProducedCache.value(m_rootMeter);
double newReturn = m_rootMeter->stateValue("totalEnergyProduced").toDouble();
// For the very first cycle (oldReturn is 0) we'll sync up on the meter values without actually adding them to our balance.
if (oldReturn == 0) {
oldReturn = newReturn;
}
if (newReturn < oldReturn) {
qCInfo(dcEnergyExperience()) << "Root meter seems to have been reset. Re-synching internal production counter.";
oldReturn = newReturn;
}
qCDebug(dcEnergyExperience()) << "Root meter total production: Previous value:" << oldReturn << "New value:" << newReturn << "Diff:" << (newReturn - oldReturn);
m_totalReturn += newReturn - oldReturn;
m_powerBalanceTotalEnergyProducedCache[m_rootMeter] = newReturn;
}
double currentPowerProduction = 0;
foreach (Thing* thing, m_thingManager->configuredThings().filterByInterface("smartmeterproducer")) {
currentPowerProduction += thing->stateValue("currentPower").toDouble();
double oldProduction = m_powerBalanceTotalEnergyProducedCache.value(thing);
double newProduction = thing->stateValue("totalEnergyProduced").toDouble();
// For the very first cycle (oldProduction is 0) we'll sync up on the producer values without actually adding them to our balance.
if (oldProduction == 0) {
oldProduction = newProduction;
}
if (newProduction < oldProduction) {
oldProduction = newProduction;
}
qCDebug(dcEnergyExperience()) << "Producer" << thing->name() << "total production: Previous value:" << oldProduction << "New value:" << newProduction << "Diff:" << (newProduction - oldProduction);
m_totalProduction += newProduction - oldProduction;
m_powerBalanceTotalEnergyProducedCache[thing] = newProduction;
}
double currentPowerStorage = 0;
double totalFromStorage = 0;
foreach (Thing *thing, m_thingManager->configuredThings().filterByInterface("energystorage")) {
currentPowerStorage += thing->stateValue("currentPower").toDouble();
double oldProduction = m_powerBalanceTotalEnergyProducedCache.value(thing);
double newProduction = thing->stateValue("totalEnergyProduced").toDouble();
// For the very first cycle (oldProdction is 0) we'll sync up on the meter values without actually adding them to our balance.
if (oldProduction == 0) {
oldProduction = newProduction;
}
if (newProduction < oldProduction) {
oldProduction = newProduction;
}
qCDebug(dcEnergyExperience()) << "Storage" << thing->name() << "total storage: Previous value:" << oldProduction << "New value:" << newProduction << "Diff:" << (newProduction - oldProduction);
totalFromStorage += newProduction - oldProduction;
m_powerBalanceTotalEnergyProducedCache[thing] = newProduction;
}
double currentPowerConsumption = currentPowerAcquisition + qAbs(qMin(0.0, currentPowerProduction)) - currentPowerStorage;
m_totalConsumption = m_totalAcquisition + m_totalProduction + totalFromStorage - m_totalReturn;
qCDebug(dcEnergyExperience()).noquote().nospace() << "Power balance: " << "🔥: " << currentPowerConsumption << " W, 🌞: " << currentPowerProduction << " W, 💵: " << currentPowerAcquisition << " W, 🔋: " << currentPowerStorage << " W. Totals: 🔥: " << m_totalConsumption << " kWh, 🌞: " << m_totalProduction << " kWh, 💵↓: " << m_totalAcquisition << " kWh, 💵↑: " << m_totalReturn << " kWh";
if (currentPowerAcquisition != m_currentPowerAcquisition
|| currentPowerConsumption != m_currentPowerConsumption
|| currentPowerProduction != m_currentPowerProduction
|| currentPowerStorage != m_currentPowerStorage) {
m_currentPowerAcquisition = currentPowerAcquisition;
m_currentPowerProduction = currentPowerProduction;
m_currentPowerConsumption = currentPowerConsumption;
m_currentPowerStorage = currentPowerStorage;
emit powerBalanceChanged();
m_logger->logPowerBalance(m_currentPowerConsumption, m_currentPowerProduction, m_currentPowerAcquisition, m_currentPowerStorage, m_totalConsumption, m_totalProduction, m_totalAcquisition, m_totalReturn);
}
}
void EnergyManagerImpl::logDumpConsumers()
{
foreach (Thing *consumer, m_thingManager->configuredThings().filterByInterface("smartmeterconsumer")) {
qCDebug(dcEnergyExperience()).nospace().noquote() << consumer->name() << ": " << (consumer->stateValue("currentPower").toDouble() / 230) << "A (" << consumer->stateValue("currentPower").toDouble() << "W)";
}
}

View File

@ -0,0 +1,76 @@
#ifndef ENERGYMANAGERIMPL_H
#define ENERGYMANAGERIMPL_H
#include <QObject>
#include <QHash>
#include <QTimer>
#include "integrations/thingmanager.h"
#include "energymanager.h"
class EnergyLogger;
class EnergyManagerImpl : public EnergyManager
{
Q_OBJECT
public:
explicit EnergyManagerImpl(ThingManager *thingManager, QObject *parent = nullptr);
Thing *rootMeter() const override;
EnergyError setRootMeter(const ThingId &rootMeterId) override;
double currentPowerConsumption() const override;
double currentPowerProduction() const override;
double currentPowerAcquisition() const override;
double currentPowerStorage() const override;
double totalConsumption() const override;
double totalProduction() const override;
double totalAcquisition() const override;
double totalReturn() const override;
EnergyLogs* logs() const override;
private:
void watchThing(Thing *thing);
void unwatchThing(const ThingId &thingId);
void updatePowerBalance();
private slots:
void logDumpConsumers();
private:
ThingManager *m_thingManager = nullptr;
Thing *m_rootMeter = nullptr;
QTimer m_balanceUpdateTimer;
double m_currentPowerConsumption = 0;
double m_currentPowerProduction = 0;
double m_currentPowerAcquisition = 0;
double m_currentPowerStorage = 0;
double m_totalConsumption = 0;
double m_totalProduction = 0;
double m_totalAcquisition = 0;
double m_totalReturn = 0;
EnergyLogger *m_logger = nullptr;
// Caching some values so we don't have to look them up on the DB all the time:
// We use different caches for power balance and thing logs because they are calculated independently
// and one must not update the others cache for the diffs to be correct
// For things totals we need to cache 2 values:
// The last thing state values we've processed
QHash<Thing*, double> m_powerBalanceTotalEnergyConsumedCache;
QHash<Thing*, double> 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<Thing*, Pair<thingStateValue, internalValue>>
QHash<Thing*, QPair<double, double>> m_thingsTotalEnergyConsumedCache;
QHash<Thing*, QPair<double, double>> m_thingsTotalEnergyProducedCache;
};
#endif // ENERGYMANAGERIMPL_H

View File

@ -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 <https://www.gnu.org/licenses/>.
*
* For any further details and any questions please contact us under
* contact@nymea.io or see our FAQ/Licensing Information on
* https://nymea.io/license/faq
*
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
#include "experiencepluginenergy.h"
#include "energymanagerimpl.h"
#include "energyjsonhandler.h"
#include "energyplugin.h"
#include <jsonrpc/jsonrpcserver.h>
#include <loggingcategories.h>
#include <QDir>
#include <QCoreApplication>
#include <QPluginLoader>
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<EnergyPlugin*>(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());
}

View File

@ -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 <https://www.gnu.org/licenses/>.
*
* For any further details and any questions please contact us under
* contact@nymea.io or see our FAQ/Licensing Information on
* https://nymea.io/license/faq
*
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
#ifndef EXPERIENCEPLUGINENERGY_H
#define EXPERIENCEPLUGINENERGY_H
#include <experiences/experienceplugin.h>
#include "energyplugin.h"
#include <QLoggingCategory>
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<EnergyPlugin*> m_plugins;
EnergyManagerImpl *m_energyManager = nullptr;
};
#endif // EXPERIENCEPLUGINENERGY_H

44
plugin/plugin.pro Normal file
View File

@ -0,0 +1,44 @@
TEMPLATE = lib
TARGET = $$qtLibraryTarget(nymea_experiencepluginenergy)
CONFIG += plugin link_pkgconfig c++11
PKGCONFIG += nymea
QT -= gui
QT += network sql
include(../config.pri)
INCLUDEPATH += $$top_srcdir/libnymea-energy
LIBS += -L$$top_builddir/libnymea-energy -lnymea-energy
HEADERS += experiencepluginenergy.h \
energyjsonhandler.h \
energylogger.h \
energymanagerimpl.h
SOURCES += experiencepluginenergy.cpp \
energyjsonhandler.cpp \
energylogger.cpp \
energymanagerimpl.cpp
target.path = $$[QT_INSTALL_LIBS]/nymea/experiences/
INSTALLS += target
# Install translation files
TRANSLATIONS *= $$files($${_PRO_FILE_PWD_}/translations/*ts, true)
lupdate.depends = FORCE
lupdate.depends += qmake_all
lupdate.commands = lupdate -recursive -no-obsolete $${_PRO_FILE_PWD_}/experience.pro
QMAKE_EXTRA_TARGETS += lupdate
# make lrelease to build .qm from .ts
lrelease.depends = FORCE
lrelease.commands += lrelease $$files($$_PRO_FILE_PWD_/translations/*.ts, true);
QMAKE_EXTRA_TARGETS += lrelease
translations.depends += lrelease
translations.path = /usr/share/nymea/translations
translations.files = $$[QT_SOURCE_TREE]/translations/*.qm
INSTALLS += translations