Merge PR #1: Add nymea energy experience plugin
This commit is contained in:
commit
dbb8885009
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
*.pro.user
|
||||
.crossbuilder/*
|
||||
10
config.pri
Normal file
10
config.pri
Normal 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
3
debian/changelog
vendored
Normal 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
1
debian/compat
vendored
Normal file
@ -0,0 +1 @@
|
||||
9
|
||||
42
debian/control
vendored
Normal file
42
debian/control
vendored
Normal 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
3
debian/libnymea-energy-dev.install.in
vendored
Normal 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
3
debian/libnymea-energy.install.in
vendored
Normal 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
|
||||
1
debian/nymea-experience-plugin-energy.install.in
vendored
Normal file
1
debian/nymea-experience-plugin-energy.install.in
vendored
Normal file
@ -0,0 +1 @@
|
||||
usr/lib/@DEB_HOST_MULTIARCH@/nymea/experiences/libnymea_experiencepluginenergy.so
|
||||
27
debian/rules
vendored
Executable file
27
debian/rules
vendored
Executable 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
|
||||
132
libnymea-energy/energylogs.cpp
Normal file
132
libnymea-energy/energylogs.cpp
Normal file
@ -0,0 +1,132 @@
|
||||
#include "energylogs.h"
|
||||
|
||||
#include <QVariant>
|
||||
|
||||
EnergyLogs::EnergyLogs(QObject *parent): QObject(parent)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
PowerBalanceLogEntry::PowerBalanceLogEntry()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
PowerBalanceLogEntry::PowerBalanceLogEntry(const QDateTime ×tamp, double consumption, double production, double acquisition, double storage, 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 ×tamp, const ThingId &thingId, double currentPower, double totalConsumption, double totalProduction):
|
||||
m_timestamp(timestamp),
|
||||
m_thingId(thingId),
|
||||
m_currentPower(currentPower),
|
||||
m_totalConsumption(totalConsumption),
|
||||
m_totalProduction(totalProduction)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
QDateTime ThingPowerLogEntry::timestamp() const
|
||||
{
|
||||
return m_timestamp;
|
||||
}
|
||||
|
||||
ThingId ThingPowerLogEntry::thingId() const
|
||||
{
|
||||
return m_thingId;
|
||||
}
|
||||
|
||||
double ThingPowerLogEntry::currentPower() const
|
||||
{
|
||||
return m_currentPower;
|
||||
}
|
||||
|
||||
double ThingPowerLogEntry::totalConsumption() const
|
||||
{
|
||||
return m_totalConsumption;
|
||||
}
|
||||
|
||||
double ThingPowerLogEntry::totalProduction() const
|
||||
{
|
||||
return m_totalProduction;
|
||||
}
|
||||
|
||||
QVariant ThingPowerLogEntries::get(int index) const
|
||||
{
|
||||
return QVariant::fromValue(at(index));
|
||||
}
|
||||
|
||||
void ThingPowerLogEntries::put(const QVariant &variant)
|
||||
{
|
||||
append(variant.value<ThingPowerLogEntry>());
|
||||
}
|
||||
139
libnymea-energy/energylogs.h
Normal file
139
libnymea-energy/energylogs.h
Normal 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 ×tamp, double consumption, double production, double acquisition, double storage, double totalConsumption, double totalProduction, double totalAcquisition, double totalReturn);
|
||||
QDateTime timestamp() const;
|
||||
double consumption() const;
|
||||
double production() const;
|
||||
double acquisition() const;
|
||||
double storage() const;
|
||||
double totalConsumption() const;
|
||||
double totalProduction() const;
|
||||
double totalAcquisition() const;
|
||||
double totalReturn() const;
|
||||
|
||||
private:
|
||||
QDateTime m_timestamp;
|
||||
double m_consumption = 0;
|
||||
double m_production = 0;
|
||||
double m_acquisition = 0;
|
||||
double m_storage = 0;
|
||||
double m_totalConsumption = 0;
|
||||
double m_totalProduction = 0;
|
||||
double m_totalAcquisition = 0;
|
||||
double m_totalReturn = 0;
|
||||
};
|
||||
Q_DECLARE_METATYPE(PowerBalanceLogEntry)
|
||||
|
||||
class PowerBalanceLogEntries: public QList<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 ×tamp, const ThingId &thingId, double currentPower, double totalConsumption, double totalProuction);
|
||||
QDateTime timestamp() const;
|
||||
ThingId thingId() const;
|
||||
double currentPower() const;
|
||||
double totalConsumption() const;
|
||||
double totalProduction() const;
|
||||
private:
|
||||
QDateTime m_timestamp;
|
||||
ThingId m_thingId;
|
||||
double m_currentPower = 0;
|
||||
double m_totalConsumption = 0;
|
||||
double m_totalProduction = 0;
|
||||
};
|
||||
Q_DECLARE_METATYPE(ThingPowerLogEntry)
|
||||
|
||||
class ThingPowerLogEntries: public QList<ThingPowerLogEntry>
|
||||
{
|
||||
Q_GADGET
|
||||
Q_PROPERTY(int count READ count)
|
||||
public:
|
||||
ThingPowerLogEntries() = default;
|
||||
ThingPowerLogEntries(const QList<ThingPowerLogEntry> &other): QList<ThingPowerLogEntry>(other) {}
|
||||
Q_INVOKABLE QVariant get(int index) const;
|
||||
Q_INVOKABLE void put(const QVariant &variant);
|
||||
};
|
||||
Q_DECLARE_METATYPE(ThingPowerLogEntries)
|
||||
|
||||
#endif // ENERGYLOGS_H
|
||||
37
libnymea-energy/energymanager.cpp
Normal file
37
libnymea-energy/energymanager.cpp
Normal 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)
|
||||
{
|
||||
|
||||
}
|
||||
76
libnymea-energy/energymanager.h
Normal file
76
libnymea-energy/energymanager.h
Normal 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
|
||||
61
libnymea-energy/energyplugin.cpp
Normal file
61
libnymea-energy/energyplugin.cpp
Normal 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();
|
||||
}
|
||||
|
||||
66
libnymea-energy/energyplugin.h
Normal file
66
libnymea-energy/energyplugin.h
Normal 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
|
||||
40
libnymea-energy/libnymea-energy.pro
Normal file
40
libnymea-energy/libnymea-energy.pro
Normal 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
|
||||
|
||||
7
nymea-experience-plugin-energy.pro
Normal file
7
nymea-experience-plugin-energy.pro
Normal file
@ -0,0 +1,7 @@
|
||||
TEMPLATE = subdirs
|
||||
|
||||
SUBDIRS += libnymea-energy plugin
|
||||
|
||||
plugin.depends = libnymea-energy
|
||||
|
||||
|
||||
204
plugin/energyjsonhandler.cpp
Normal file
204
plugin/energyjsonhandler.cpp
Normal 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 ¶ms)
|
||||
{
|
||||
Q_UNUSED(params)
|
||||
QVariantMap ret;
|
||||
if (m_energyManager->rootMeter()) {
|
||||
ret.insert("rootMeterThingId", m_energyManager->rootMeter()->id());
|
||||
}
|
||||
return createReply(ret);
|
||||
}
|
||||
|
||||
JsonReply *EnergyJsonHandler::SetRootMeter(const QVariantMap ¶ms)
|
||||
{
|
||||
QVariantMap returns;
|
||||
|
||||
if (!params.contains("rootMeterThingId")) {
|
||||
returns.insert("energyError", enumValueName(EnergyManager::EnergyErrorMissingParameter));
|
||||
return createReply(returns);
|
||||
}
|
||||
EnergyManager::EnergyError status = m_energyManager->setRootMeter(params.value("rootMeterThingId").toUuid());
|
||||
returns.insert("energyError", enumValueName(status));
|
||||
return createReply(returns);
|
||||
}
|
||||
|
||||
JsonReply *EnergyJsonHandler::GetPowerBalance(const QVariantMap ¶ms)
|
||||
{
|
||||
Q_UNUSED(params)
|
||||
QVariantMap ret;
|
||||
ret.insert("currentPowerConsumption", m_energyManager->currentPowerConsumption());
|
||||
ret.insert("currentPowerProduction", m_energyManager->currentPowerProduction());
|
||||
ret.insert("currentPowerAcquisition", m_energyManager->currentPowerAcquisition());
|
||||
ret.insert("currentPowerStorage", m_energyManager->currentPowerStorage());
|
||||
ret.insert("totalConsumption", m_energyManager->totalConsumption());
|
||||
ret.insert("totalProduction", m_energyManager->totalProduction());
|
||||
ret.insert("totalAcquisition", m_energyManager->totalAcquisition());
|
||||
ret.insert("totalReturn", m_energyManager->totalReturn());
|
||||
return createReply(ret);
|
||||
}
|
||||
|
||||
JsonReply *EnergyJsonHandler::GetPowerBalanceLogs(const QVariantMap ¶ms)
|
||||
{
|
||||
EnergyLogs::SampleRate sampleRate = enumNameToValue<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 ¶ms)
|
||||
{
|
||||
EnergyLogs::SampleRate sampleRate = enumNameToValue<EnergyLogs::SampleRate>(params.value("sampleRate").toString());
|
||||
QList<ThingId> thingIds;
|
||||
foreach (const QVariant &thingId, params.value("thingIds").toList()) {
|
||||
thingIds.append(thingId.toUuid());
|
||||
}
|
||||
QDateTime from = params.contains("from") ? QDateTime::fromMSecsSinceEpoch(params.value("from").toLongLong() * 1000) : QDateTime();
|
||||
QDateTime to = params.contains("to") ? QDateTime::fromMSecsSinceEpoch(params.value("to").toLongLong() * 1000) : QDateTime();
|
||||
QVariantMap returns;
|
||||
returns.insert("thingPowerLogEntries", pack(m_energyManager->logs()->thingPowerLogs(sampleRate, thingIds, from, to)));
|
||||
|
||||
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);
|
||||
}
|
||||
33
plugin/energyjsonhandler.h
Normal file
33
plugin/energyjsonhandler.h
Normal 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 ¶ms);
|
||||
Q_INVOKABLE JsonReply* SetRootMeter(const QVariantMap ¶ms);
|
||||
Q_INVOKABLE JsonReply* GetPowerBalance(const QVariantMap ¶ms);
|
||||
Q_INVOKABLE JsonReply* GetPowerBalanceLogs(const QVariantMap ¶ms);
|
||||
Q_INVOKABLE JsonReply* GetThingPowerLogs(const QVariantMap ¶ms);
|
||||
|
||||
signals:
|
||||
void RootMeterChanged(const QVariantMap ¶ms);
|
||||
void PowerBalanceChanged(const QVariantMap ¶ms);
|
||||
void PowerBalanceLogEntryAdded(const QVariantMap ¶ms);
|
||||
void ThingPowerLogEntryAdded(const QVariantMap ¶ms);
|
||||
|
||||
private:
|
||||
EnergyManager *m_energyManager = nullptr;
|
||||
};
|
||||
|
||||
#endif // ENERGYJSONHANDLER_H
|
||||
876
plugin/energylogger.cpp
Normal file
876
plugin/energylogger.cpp
Normal 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 ×tamp, SampleRate sampleRate, double consumption, double production, double acquisition, double storage, double totalConsumption, double totalProduction, double totalAcquisition, double totalReturn)
|
||||
{
|
||||
QSqlQuery query = QSqlQuery(m_db);
|
||||
query.prepare("INSERT INTO powerBalance (timestamp, sampleRate, consumption, production, acquisition, storage, totalConsumption, totalProduction, totalAcquisition, totalReturn) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?);");
|
||||
query.addBindValue(timestamp.toMSecsSinceEpoch());
|
||||
query.addBindValue(sampleRate);
|
||||
query.addBindValue(consumption);
|
||||
query.addBindValue(production);
|
||||
query.addBindValue(acquisition);
|
||||
query.addBindValue(storage);
|
||||
query.addBindValue(totalConsumption);
|
||||
query.addBindValue(totalProduction);
|
||||
query.addBindValue(totalAcquisition);
|
||||
query.addBindValue(totalReturn);
|
||||
query.exec();
|
||||
if (query.lastError().isValid()) {
|
||||
qCWarning(dcEnergyExperience()) << "Error logging consumption sample:" << query.lastError() << query.executedQuery();
|
||||
return false;
|
||||
}
|
||||
emit powerBalanceEntryAdded(sampleRate, PowerBalanceLogEntry(timestamp, consumption, production, acquisition, storage, totalConsumption, totalProduction, totalAcquisition, totalReturn));
|
||||
return true;
|
||||
}
|
||||
|
||||
bool EnergyLogger::sampleThingsPower(SampleRate sampleRate, SampleRate baseSampleRate, const QDateTime &sampleEnd)
|
||||
{
|
||||
bool ret = true;
|
||||
foreach (const ThingId &thingId, m_thingsPowerLiveLogs.keys()) {
|
||||
ret &= sampleThingPower(thingId, sampleRate, baseSampleRate, sampleEnd);
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
bool EnergyLogger::sampleThingPower(const ThingId &thingId, SampleRate sampleRate, SampleRate baseSampleRate, const QDateTime &sampleEnd)
|
||||
{
|
||||
QDateTime sampleStart = sampleEnd.addMSecs(-sampleRate * 60 * 1000);
|
||||
|
||||
qCDebug(dcEnergyExperience()) << "Sampling thing power for" << thingId.toString() << sampleRate << "from" << sampleStart.toString() << "to" << sampleEnd.toString();
|
||||
|
||||
double medianCurrentPower = 0;
|
||||
double totalConsumption = 0;
|
||||
double totalProduction = 0;
|
||||
|
||||
QSqlQuery query(m_db);
|
||||
query.prepare("SELECT * FROM thingPower WHERE thingId = ? AND sampleRate = ? AND timestamp > ? AND timestamp <= ?;");
|
||||
query.addBindValue(thingId);
|
||||
query.addBindValue(baseSampleRate);
|
||||
query.addBindValue(sampleStart.toMSecsSinceEpoch());
|
||||
query.addBindValue(sampleEnd.toMSecsSinceEpoch());
|
||||
query.exec();
|
||||
|
||||
if (query.lastError().isValid()) {
|
||||
qCWarning(dcEnergyExperience()) << "Error fetching thing power samples for" << baseSampleRate << "from" << sampleStart.toString() << "to" << sampleEnd.toString();
|
||||
qCWarning(dcEnergyExperience()) << "SQL error was:" << query.lastError() << "executed query:" << query.executedQuery();
|
||||
return false;
|
||||
}
|
||||
|
||||
qCDebug(dcEnergyExperience()) << "Query:" << query.executedQuery();
|
||||
qCDebug(dcEnergyExperience()) << "Results:" << query.size();
|
||||
|
||||
int resultCount = 0;
|
||||
while (query.next()) {
|
||||
resultCount++;
|
||||
qCDebug(dcEnergyExperience()) << "Frame:" << query.value("currentPower").toDouble() << QDateTime::fromMSecsSinceEpoch(query.value("timestamp").toLongLong()).toString();
|
||||
medianCurrentPower += query.value("currentPower").toDouble();
|
||||
totalConsumption = query.value("totalConsumption").toDouble();
|
||||
totalProduction = query.value("totalProduction").toDouble();
|
||||
}
|
||||
if (resultCount > 0) {
|
||||
medianCurrentPower = medianCurrentPower * baseSampleRate / sampleRate;
|
||||
|
||||
} else {
|
||||
// If there are no base samples for the given time frame at all, let's try to find the last existing one in the base
|
||||
// to at least copy the totals from where we left off.
|
||||
|
||||
query = QSqlQuery(m_db);
|
||||
query.prepare("SELECT MAX(timestamp), currentPower, totalConsumption, totalProduction FROM thingPower WHERE thingId = ? AND sampleRate = ?;");
|
||||
query.addBindValue(thingId);
|
||||
query.addBindValue(baseSampleRate);
|
||||
query.exec();
|
||||
if (query.lastError().isValid()) {
|
||||
qCWarning(dcEnergyExperience()) << "Error fetching newest thing power sample for" << thingId.toString() << baseSampleRate;
|
||||
qCWarning(dcEnergyExperience()) << "SQL error was:" << query.lastError() << "executed query:" << query.executedQuery();
|
||||
return false;
|
||||
}
|
||||
|
||||
if (query.next()) {
|
||||
totalConsumption = query.value("totalConsumption").toDouble();
|
||||
totalProduction = query.value("totalProduction").toDouble();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
qCDebug(dcEnergyExperience()) << "Sampled:" << thingId.toString() << sampleRate << "median currentPower:" << medianCurrentPower << "total consumption:" << totalConsumption << "total production:" << totalProduction;
|
||||
return insertThingPower(sampleEnd, sampleRate, thingId, medianCurrentPower, totalConsumption, totalProduction);
|
||||
}
|
||||
|
||||
bool EnergyLogger::insertThingPower(const QDateTime ×tamp, SampleRate sampleRate, const ThingId &thingId, double currentPower, double totalConsumption, double totalProduction)
|
||||
{
|
||||
QSqlQuery query = QSqlQuery(m_db);
|
||||
query.prepare("INSERT INTO thingPower (timestamp, sampleRate, thingId, currentPower, totalConsumption, totalProduction) values (?, ?, ?, ?, ?, ?);");
|
||||
query.addBindValue(timestamp.toMSecsSinceEpoch());
|
||||
query.addBindValue(sampleRate);
|
||||
query.addBindValue(thingId);
|
||||
query.addBindValue(currentPower);
|
||||
query.addBindValue(totalConsumption);
|
||||
query.addBindValue(totalProduction);
|
||||
query.exec();
|
||||
if (query.lastError().isValid()) {
|
||||
qCWarning(dcEnergyExperience()) << "Error logging thing power sample:" << query.lastError() << query.executedQuery();
|
||||
return false;
|
||||
}
|
||||
emit thingPowerEntryAdded(sampleRate, ThingPowerLogEntry(timestamp, thingId, currentPower, totalConsumption, totalProduction));
|
||||
return true;
|
||||
}
|
||||
|
||||
void EnergyLogger::trimPowerBalance(SampleRate sampleRate, const QDateTime &beforeTime)
|
||||
{
|
||||
QSqlQuery query(m_db);
|
||||
query.prepare("DELETE FROM powerBalance WHERE sampleRate = ? AND timestamp < ?;");
|
||||
query.addBindValue(sampleRate);
|
||||
query.addBindValue(beforeTime.toMSecsSinceEpoch());
|
||||
query.exec();
|
||||
if (query.numRowsAffected() > 0) {
|
||||
qCDebug(dcEnergyExperience()).nospace() << "Trimmed " << query.numRowsAffected() << " from power balance series: " << sampleRate << " (Older than: " << beforeTime.toString() << ")";
|
||||
}
|
||||
}
|
||||
|
||||
void EnergyLogger::trimThingPower(const ThingId &thingId, SampleRate sampleRate, const QDateTime &beforeTime)
|
||||
{
|
||||
QSqlQuery query(m_db);
|
||||
query.prepare("DELETE FROM thingPower WHERE thingId = ? AND sampleRate = ? AND timestamp < ?;");
|
||||
query.addBindValue(thingId);
|
||||
query.addBindValue(sampleRate);
|
||||
query.addBindValue(beforeTime.toMSecsSinceEpoch());
|
||||
query.exec();
|
||||
if (query.numRowsAffected() > 0) {
|
||||
qCDebug(dcEnergyExperience()).nospace() << "Trimmed " << query.numRowsAffected() << " from thing power series for: " << thingId << sampleRate << " (Older than: " << beforeTime.toString() << ")";
|
||||
}
|
||||
}
|
||||
|
||||
PowerBalanceLogEntry EnergyLogger::queryResultToBalanceLogEntry(const QSqlRecord &record) const
|
||||
{
|
||||
return PowerBalanceLogEntry(QDateTime::fromMSecsSinceEpoch(record.value("timestamp").toLongLong()),
|
||||
record.value("consumption").toDouble(),
|
||||
record.value("production").toDouble(),
|
||||
record.value("acquisition").toDouble(),
|
||||
record.value("storage").toDouble(),
|
||||
record.value("totalConsumption").toDouble(),
|
||||
record.value("totalProduction").toDouble(),
|
||||
record.value("totalAcquisition").toDouble(),
|
||||
record.value("totalReturn").toDouble());
|
||||
|
||||
}
|
||||
|
||||
ThingPowerLogEntry EnergyLogger::queryResultToThingPowerLogEntry(const QSqlRecord &record) const
|
||||
{
|
||||
return ThingPowerLogEntry(QDateTime::fromMSecsSinceEpoch(record.value("timestamp").toULongLong()),
|
||||
record.value("thingId").toUuid(),
|
||||
record.value("currentPower").toDouble(),
|
||||
record.value("totalConsumption").toDouble(),
|
||||
record.value("totalProduction").toDouble());
|
||||
}
|
||||
84
plugin/energylogger.h
Normal file
84
plugin/energylogger.h
Normal 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 ×tamp, SampleRate sampleRate, double consumption, double production, double acquisition, double storage, double totalConsumption, double totalProduction, double totalAcquisition, double totalReturn);
|
||||
bool sampleThingsPower(SampleRate sampleRate, SampleRate baseSampleRate, const QDateTime &sampleEnd);
|
||||
bool sampleThingPower(const ThingId &thingId, SampleRate sampleRate, SampleRate baseSampleRate, const QDateTime &sampleEnd);
|
||||
bool insertThingPower(const QDateTime ×tamp, SampleRate sampleRate, const ThingId &thingId, double currentPower, double totalConsumption, double totalProduction);
|
||||
void trimPowerBalance(SampleRate sampleRate, const QDateTime &beforeTime);
|
||||
void trimThingPower(const ThingId &thingId, SampleRate sampleRate, const QDateTime &beforeTime);
|
||||
|
||||
PowerBalanceLogEntry queryResultToBalanceLogEntry(const QSqlRecord &record) const;
|
||||
ThingPowerLogEntry queryResultToThingPowerLogEntry(const QSqlRecord &record) const;
|
||||
|
||||
private:
|
||||
struct SampleConfig {
|
||||
SampleRate baseSampleRate;
|
||||
int maxSamples = 0;
|
||||
};
|
||||
|
||||
PowerBalanceLogEntries m_balanceLiveLog;
|
||||
QHash<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
|
||||
319
plugin/energymanagerimpl.cpp
Normal file
319
plugin/energymanagerimpl.cpp
Normal 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)";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
76
plugin/energymanagerimpl.h
Normal file
76
plugin/energymanagerimpl.h
Normal 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
|
||||
119
plugin/experiencepluginenergy.cpp
Normal file
119
plugin/experiencepluginenergy.cpp
Normal 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());
|
||||
|
||||
}
|
||||
64
plugin/experiencepluginenergy.h
Normal file
64
plugin/experiencepluginenergy.h
Normal 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
44
plugin/plugin.pro
Normal 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
|
||||
|
||||
Reference in New Issue
Block a user