621 lines
25 KiB
C++
621 lines
25 KiB
C++
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
|
|
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
|
|
*
|
|
* Copyright (C) 2013 - 2024, nymea GmbH
|
|
* Copyright (C) 2024 - 2025, chargebyte austria GmbH
|
|
*
|
|
* This file is part of nymea-energy-plugin-nymea.
|
|
*
|
|
* nymea-energy-plugin-nymea.s free software: you can redistribute it and/or modify
|
|
* it under the terms of the GNU General Public License as published by
|
|
* the Free Software Foundation, either version 3 of the License, or
|
|
* (at your option) any later version.
|
|
*
|
|
* nymea-energy-plugin-nymea.s 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 nymea-energy-plugin-nymea. If not, see <https://www.gnu.org/licenses/>.
|
|
*
|
|
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
|
|
|
|
#include "spotmarketmanager.h"
|
|
#include "../plugininfo.h"
|
|
#include "../energysettings.h"
|
|
|
|
#include "spotmarketdataproviderawattar.h"
|
|
|
|
SpotMarketManager::SpotMarketManager(QNetworkAccessManager *networkManager, QObject *parent) :
|
|
QObject{parent},
|
|
m_networkManager{networkManager}
|
|
{
|
|
// Init known spot market providers
|
|
registerProvider(new SpotMarketDataProviderAwattar(m_networkManager, SpotMarketDataProviderAwattar::AwattarCountryAustria, this));
|
|
registerProvider(new SpotMarketDataProviderAwattar(m_networkManager, SpotMarketDataProviderAwattar::AwattarCountryGermany, this));
|
|
|
|
// Load settings
|
|
EnergySettings settings;
|
|
settings.beginGroup("SpotMarket");
|
|
setEnabled(settings.value("enabled", false).toBool());
|
|
settings.endGroup(); // SpotMarket
|
|
}
|
|
|
|
bool SpotMarketManager::enabled() const
|
|
{
|
|
return m_enabled;
|
|
}
|
|
|
|
void SpotMarketManager::setEnabled(bool enabled)
|
|
{
|
|
EnergySettings settings;
|
|
settings.beginGroup("SpotMarket");
|
|
|
|
if (enabled) {
|
|
qCDebug(dcNymeaEnergy()) << "SpotMarketManager: Enable spot market manager";
|
|
|
|
QUuid providerId = settings.value("providerId").toUuid();
|
|
if (providerId.isNull()) {
|
|
qCDebug(dcNymeaEnergy()) << "SpotMarketManager: Currently no spot market provider configured. Selecting the first available.";
|
|
changeProvider(m_availableProviders.keys().first());
|
|
} else {
|
|
changeProvider(providerId);
|
|
}
|
|
|
|
m_currentProvider->enable();
|
|
qCInfo(dcNymeaEnergy()) << "SpotMarketManager: Enabled using" << m_currentProvider;
|
|
|
|
} else {
|
|
qCDebug(dcNymeaEnergy()) << "SpotMarketManager: Disable spot market manager.";
|
|
if (m_currentProvider) {
|
|
m_currentProvider->disable();
|
|
}
|
|
}
|
|
|
|
if (m_enabled != enabled) {
|
|
m_enabled = enabled;
|
|
settings.setValue("enabled", m_enabled);
|
|
emit enabledChanged(m_enabled);
|
|
}
|
|
|
|
settings.endGroup(); // SpotMarket
|
|
}
|
|
|
|
bool SpotMarketManager::available() const
|
|
{
|
|
if (!m_currentProvider)
|
|
return false;
|
|
|
|
return m_currentProvider->available();
|
|
}
|
|
|
|
SpotMarketDataProvider *SpotMarketManager::currentProvider() const
|
|
{
|
|
return m_currentProvider;
|
|
}
|
|
|
|
QUuid SpotMarketManager::currentProviderId() const
|
|
{
|
|
if (m_currentProvider)
|
|
return m_currentProvider->providerId();
|
|
|
|
return QUuid();
|
|
}
|
|
|
|
SpotMarketProviderInfos SpotMarketManager::availableProviders() const
|
|
{
|
|
return m_availableProviderInfos;
|
|
}
|
|
|
|
bool SpotMarketManager::changeProvider(const QUuid &providerId)
|
|
{
|
|
if (providerId.isNull() || !m_availableProviders.contains(providerId)) {
|
|
qCWarning(dcNymeaEnergy()) << "SpotMarketManager: Requested to change provider to" << providerId.toString() << "but there is no such provider available.";
|
|
return false;
|
|
}
|
|
|
|
if (m_currentProvider) {
|
|
qCDebug(dcNymeaEnergy()) << "SpotMarketManager: Unset current provider" << m_currentProvider;
|
|
m_currentProvider->disable();
|
|
|
|
// Disconnect everything from availabeChanged
|
|
disconnect(m_currentProvider, &SpotMarketDataProvider::availableChanged, this, &SpotMarketManager::availableChanged);
|
|
disconnect(m_currentProvider, &SpotMarketDataProvider::enabledChanged, this, &SpotMarketManager::enabledChanged);
|
|
disconnect(m_currentProvider, &SpotMarketDataProvider::scoreEntriesChanged, this, &SpotMarketManager::onProviderScoreEntriesChanged);
|
|
|
|
m_currentProvider = nullptr;
|
|
}
|
|
|
|
m_weightedScores.clear();
|
|
|
|
qCDebug(dcNymeaEnergy()) << "SpotMarketManager: Changing provider to" << m_availableProviders.value(providerId);
|
|
m_currentProvider = m_availableProviders.value(providerId);
|
|
emit currentProviderChanged(m_currentProvider);
|
|
|
|
connect(m_currentProvider, &SpotMarketDataProvider::availableChanged, this, &SpotMarketManager::availableChanged);
|
|
connect(m_currentProvider, &SpotMarketDataProvider::enabledChanged, this, &SpotMarketManager::enabledChanged);
|
|
connect(m_currentProvider, &SpotMarketDataProvider::scoreEntriesChanged, this, &SpotMarketManager::onProviderScoreEntriesChanged);
|
|
|
|
EnergySettings settings;
|
|
settings.beginGroup("SpotMarket");
|
|
settings.setValue("providerId", m_currentProvider->providerId());
|
|
settings.endGroup(); // SpotMarket
|
|
return true;
|
|
}
|
|
|
|
bool SpotMarketManager::registerProvider(SpotMarketDataProvider *provider)
|
|
{
|
|
if (m_availableProviders.contains(provider->providerId())) {
|
|
qCWarning(dcNymeaEnergy()) << "SpotMarketManager: Try to register already registered provider. Ignoring request.";
|
|
return false;
|
|
}
|
|
|
|
m_availableProviders.insert(provider->providerId(), provider);
|
|
m_availableProviderInfos.append(provider->info());
|
|
emit availableProvidersChanged();
|
|
qCDebug(dcNymeaEnergy()) << "SpotMarketManager: Registered" << provider;
|
|
return true;
|
|
}
|
|
|
|
ScoreEntries SpotMarketManager::weightedScoreEntries(const QDate &date) const
|
|
{
|
|
if (!m_currentProvider)
|
|
return ScoreEntries();
|
|
|
|
if (!date.isValid())
|
|
return weightScoreEntries(m_currentProvider->scoreEntries());
|
|
|
|
return m_weightedScores.value(date);
|
|
}
|
|
|
|
ScoreEntries SpotMarketManager::weightScoreEntries(const ScoreEntries &scoreEntries)
|
|
{
|
|
bool dataInitialized = false; double bestPrice = 0; double worstPrice = 0;
|
|
foreach (const ScoreEntry &entry, scoreEntries) {
|
|
if (!dataInitialized) {
|
|
bestPrice = entry.value();
|
|
worstPrice = entry.value();
|
|
dataInitialized = true;
|
|
continue;
|
|
}
|
|
|
|
if (entry.value() < bestPrice) {
|
|
bestPrice = entry.value();
|
|
}
|
|
|
|
if (entry.value() > worstPrice) {
|
|
worstPrice = entry.value();
|
|
}
|
|
}
|
|
|
|
ScoreEntries weigtedEntries;
|
|
foreach (const ScoreEntry &entry, scoreEntries) {
|
|
ScoreEntry newEntry = entry;
|
|
newEntry.setWeighting((newEntry.value() - worstPrice) / (bestPrice - worstPrice));
|
|
weigtedEntries.append(newEntry);
|
|
}
|
|
|
|
weigtedEntries.sortByStartDateTime();
|
|
|
|
if (weigtedEntries.isEmpty()) {
|
|
qCDebug(dcNymeaEnergy()) << "Weigted" << scoreEntries.count() << "score entries";
|
|
} else {
|
|
qCDebug(dcNymeaEnergy()) << "Weigted" << scoreEntries.count() << "score entries"
|
|
<< weigtedEntries.first().startDateTime().toString("dd.MM.yyyy hh:mm")
|
|
<< "-"
|
|
<< weigtedEntries.last().endDateTime().toString("dd.MM.yyyy hh:mm")
|
|
<< "Best price:" << bestPrice << "(1.0)"
|
|
<< "Worst price:" << worstPrice << "(0.0)";
|
|
}
|
|
|
|
return weigtedEntries;
|
|
}
|
|
|
|
TimeFrames SpotMarketManager::scheduleCharingTimeForToday(const QDateTime ¤tDateTime, const uint minutes, uint minimumScheduleDuration, bool currentFrameLocked)
|
|
{
|
|
const ScoreEntries weightedScores = m_weightedScores.value(currentDateTime.date());
|
|
return scheduleChargingTime(currentDateTime, weightedScores, minutes, minimumScheduleDuration, currentFrameLocked);
|
|
}
|
|
|
|
TimeFrames SpotMarketManager::scheduleChargingTime(const QDateTime ¤tDateTime, const ScoreEntries weightedScores, const uint minutes, uint minimumScheduleDuration, bool currentFrameLocked)
|
|
{
|
|
TimeFrames timeFrames;
|
|
ScoreEntries availableScores;
|
|
|
|
for (int i = 0; i < weightedScores.count(); i++) {
|
|
const ScoreEntry scoreEntry = weightedScores.at(i);
|
|
// We can not change the past, even if we would like to that some times...
|
|
if (scoreEntry.endDateTime() < currentDateTime)
|
|
continue;
|
|
|
|
// If this is the current hour, let's update the start time so we have later the correct timeframe available
|
|
if (scoreEntry.isActive(currentDateTime)) {
|
|
ScoreEntry newEntry = scoreEntry;
|
|
newEntry.setStartDateTime(currentDateTime);
|
|
availableScores.append(newEntry);
|
|
} else {
|
|
availableScores.append(scoreEntry);
|
|
}
|
|
}
|
|
|
|
uint minutesLeftToSchedule = 0;
|
|
foreach (const ScoreEntry &score, availableScores) {
|
|
minutesLeftToSchedule += score.durationMinutes();
|
|
}
|
|
|
|
// Make sure we have enougth schedules left, otherwise return what we have...
|
|
if (minutes >= minutesLeftToSchedule) {
|
|
qCDebug(dcNymeaEnergy()) << "Not enought score entries left in order to schedule" << minutes << "minutes. Using the leftover schedules beeing a total of" << minutesLeftToSchedule << "minutes.";
|
|
foreach(const ScoreEntry &scoreEntry, availableScores) {
|
|
timeFrames.append(TimeFrame(scoreEntry.startDateTime(), scoreEntry.endDateTime()));
|
|
}
|
|
} else {
|
|
// Sort the schedules by weighting
|
|
availableScores.sortByWeighting();
|
|
//qCDebug(dcNymeaEnergy()) << "Sorted by weighting" << availableScores;
|
|
|
|
uint minutesLeft = minutes;
|
|
int currentIndex = 0;
|
|
while(minutesLeft > 0 && currentIndex < availableScores.count()) {
|
|
const ScoreEntry currentScore = availableScores.at(currentIndex);
|
|
TimeFrame newFrame;
|
|
if (minutesLeft >= 60) {
|
|
newFrame = TimeFrame(currentScore.startDateTime(), currentScore.endDateTime());
|
|
} else {
|
|
// Partial hour, let's see how many minutes we have left within this hour
|
|
uint minutesDuration = 60 - currentScore.startDateTime().time().minute();
|
|
if (minutesDuration < minutesLeft) {
|
|
newFrame = TimeFrame(currentScore.startDateTime(), currentScore.startDateTime().addSecs(minutesDuration * 60));
|
|
} else {
|
|
newFrame = TimeFrame(currentScore.startDateTime(), currentScore.startDateTime().addSecs(minutesLeft * 60));
|
|
}
|
|
}
|
|
|
|
timeFrames.append(newFrame);
|
|
minutesLeft -= newFrame.durationMinutes();
|
|
currentIndex++;
|
|
}
|
|
}
|
|
|
|
|
|
|
|
TimeFrames fusedFrames = fuseTimeFrames(timeFrames);
|
|
|
|
// Now make sure all frames respect the minimum schedule duration.
|
|
// The reason for a minimum duration might be the fact that we want to
|
|
// charge as continuouse as possible. We don't want to switch the charging on and off
|
|
// in less i.e. than 10 min. We append or prepend the rest minutes on an existing frame
|
|
// using the best possible side.
|
|
|
|
// This is the price we have to pay for continuouse charging
|
|
|
|
foreach (const TimeFrame &frame, fusedFrames) {
|
|
|
|
// If we are currently loading and the frame has moved less than 10 minutes into the future, we move the window to now in otder to prevent jutter
|
|
if (currentFrameLocked && currentDateTime.msecsTo(frame.startDateTime()) < 600000) {
|
|
int secondsOffset = qRound((frame.startDateTime().toMSecsSinceEpoch() - currentDateTime.toMSecsSinceEpoch()) / 1000.0);
|
|
// Move the current frame by the offset to now since we where already charging
|
|
TimeFrame movedFrame = frame;
|
|
movedFrame.setStartDateTime(frame.startDateTime().addSecs(-secondsOffset));
|
|
movedFrame.setEndDateTime(frame.endDateTime().addSecs(-secondsOffset));
|
|
fusedFrames.removeAll(frame);
|
|
fusedFrames.append(movedFrame);
|
|
qCDebug(dcNymeaEnergy()) << "Moving frame to current date time" << secondsOffset / 60.0 << "minutes";
|
|
// We are done here...
|
|
break;
|
|
}
|
|
|
|
if (minimumScheduleDuration > 1 && frame.durationMinutes() < minimumScheduleDuration) {
|
|
|
|
// If this is the current frame, and it is not locked (because we are currently charging), allow to finish it
|
|
if (frame.isActive(currentDateTime) && currentFrameLocked) {
|
|
// Keep this schedule even it does not meet the minimum schedule durarion because the current frame is locked
|
|
break;
|
|
}
|
|
|
|
// This frame does not meet our minimum duration, distribute the rest to the next best schedule
|
|
fusedFrames.removeAll(frame);
|
|
|
|
// Possible option: for now, we just append the remaining time to the shortest time frame
|
|
// in favour of append/prepend it to the best price hour. A small price we are willing to pay in favour of
|
|
// a more constant charging, a car battery is more expensice than this small amount of energy.
|
|
|
|
int shortestFrameIndex = -1;
|
|
uint shortestFrameDuration = 0;
|
|
for (int i = 0; i < fusedFrames.count(); i++) {
|
|
const TimeFrame currentFrame = fusedFrames.at(i);
|
|
|
|
// The sooner the better if 2 have the same duration
|
|
if (currentFrame.durationMinutes() < shortestFrameDuration || shortestFrameIndex < 0) {
|
|
shortestFrameDuration = currentFrame.durationMinutes();
|
|
shortestFrameIndex = i;
|
|
}
|
|
}
|
|
|
|
// There is no other frame we can append, let's re-add it...
|
|
if (shortestFrameIndex < 0) {
|
|
// FIXME: maybe try to add this one into the next day, it's not the current locked frame so do not toggle charging
|
|
fusedFrames.append(frame);
|
|
break;
|
|
}
|
|
|
|
// Add the to short frame to the shortest time frame in order to be as continoiuse as possible regarding charging changes.
|
|
fusedFrames[shortestFrameIndex].setEndDateTime(fusedFrames.value(shortestFrameIndex).endDateTime().addSecs(frame.durationMinutes() * 60));
|
|
|
|
// Note: there can only be one schedule which might has to be distributed...we are done
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Return them sorted by date
|
|
std::sort(fusedFrames.begin(), fusedFrames.end(), [](const TimeFrame &a, const TimeFrame &b) -> bool {
|
|
return a.startDateTime() < b.startDateTime();
|
|
});
|
|
|
|
return fusedFrames;
|
|
}
|
|
|
|
TimeFrames SpotMarketManager::fuseTimeFrames(const TimeFrames &timeFrames)
|
|
{
|
|
if (timeFrames.count() <= 1)
|
|
return timeFrames;
|
|
|
|
TimeFrames fusedFrames, partialHours;
|
|
|
|
// First fuse all full hours
|
|
for (int i = 0; i < timeFrames.count(); i++) {
|
|
const TimeFrame currentFrame = timeFrames.at(i);
|
|
int frameDurationMinutes = (currentFrame.endDateTime().toMSecsSinceEpoch() - currentFrame.startDateTime().toMSecsSinceEpoch()) / 60000;
|
|
|
|
// We take care about the partial hours later
|
|
if (frameDurationMinutes < 60) {
|
|
partialHours.append(currentFrame);
|
|
continue;
|
|
}
|
|
|
|
// First full hour...just add, nothing to fuse
|
|
if (fusedFrames.isEmpty()) {
|
|
fusedFrames.append(currentFrame);
|
|
continue;
|
|
}
|
|
|
|
// Check if we can extend an existing frame
|
|
bool extended = false;
|
|
for (int j = 0; j < fusedFrames.count(); j++) {
|
|
if (currentFrame.endDateTime() == fusedFrames.at(j).startDateTime()) {
|
|
// Prepend hour
|
|
fusedFrames[j].setStartDateTime(currentFrame.startDateTime());
|
|
extended = true;
|
|
break;
|
|
} else if (currentFrame.startDateTime() == fusedFrames.at(j).endDateTime()) {
|
|
// Append hour
|
|
fusedFrames[j].setEndDateTime(currentFrame.endDateTime());
|
|
extended = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!extended) {
|
|
// Add the full hour to the fused frames
|
|
fusedFrames.append(currentFrame);
|
|
continue;
|
|
}
|
|
}
|
|
|
|
|
|
TimeFrames partialHoursUnfused;
|
|
|
|
// Now try to fuse the partial hours into the fused full hours ...
|
|
for (int i = 0; i < partialHours.count(); i++) {
|
|
const TimeFrame currentFrame = partialHours.at(i);
|
|
TimeFrame currentFrameMovedToEnd;
|
|
if (currentFrame.endDateTime().time().minute() == 0) {
|
|
// This time frame is already at the end. No need to move
|
|
currentFrameMovedToEnd = currentFrame;
|
|
} else {
|
|
int endtimeOffset = 60 - currentFrame.endDateTime().time().minute();
|
|
QDateTime endDateTime = currentFrame.endDateTime().addSecs(endtimeOffset * 60);
|
|
currentFrameMovedToEnd.setEndDateTime(endDateTime);
|
|
currentFrameMovedToEnd.setStartDateTime(endDateTime.addSecs(static_cast<int>(currentFrame.durationMinutes()) * -60));
|
|
}
|
|
|
|
// Check if we can extend an existing frame
|
|
bool extended = false;
|
|
|
|
// Try to fuse the start partial frame
|
|
for (int j = 0; j < fusedFrames.count(); j++) {
|
|
if (currentFrame.endDateTime() == fusedFrames.at(j).startDateTime()) {
|
|
// Prepend partial hour
|
|
fusedFrames[j].setStartDateTime(currentFrame.startDateTime());
|
|
extended = true;
|
|
break;
|
|
} else if (currentFrame.startDateTime() == fusedFrames.at(j).endDateTime()) {
|
|
// Append partial hour
|
|
fusedFrames[j].setEndDateTime(currentFrame.endDateTime());
|
|
extended = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!extended && currentFrame != currentFrameMovedToEnd) {
|
|
// Try to fuse the end partial frame
|
|
for (int j = 0; j < fusedFrames.count(); j++) {
|
|
|
|
if (currentFrameMovedToEnd.endDateTime() == fusedFrames.at(j).startDateTime()) {
|
|
// Prepend partial hour
|
|
fusedFrames[j].setStartDateTime(currentFrameMovedToEnd.startDateTime());
|
|
extended = true;
|
|
break;
|
|
} else if (currentFrameMovedToEnd.startDateTime() == fusedFrames.at(j).endDateTime()) {
|
|
// Append partial hour
|
|
fusedFrames[j].setEndDateTime(currentFrameMovedToEnd.endDateTime());
|
|
extended = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!extended) {
|
|
// Note: we have a standalone partial hour...add it as is,
|
|
// it might overlap into an other hour or bet the current,
|
|
// we don't know here, so let's not change it. Do not use
|
|
// the moved to end frame since that destroyes the upper logic.
|
|
partialHoursUnfused.append(currentFrame);
|
|
continue;
|
|
}
|
|
}
|
|
|
|
if (!partialHoursUnfused.isEmpty()) {
|
|
TimeFrames fusedPartialFrames = fusePartialTimeFrames(partialHoursUnfused);
|
|
fusedFrames.append(fusedPartialFrames);
|
|
}
|
|
|
|
// Return them sorted by date
|
|
std::sort(fusedFrames.begin(), fusedFrames.end(), [](const TimeFrame &a, const TimeFrame &b) -> bool {
|
|
return a.startDateTime() < b.startDateTime();
|
|
});
|
|
|
|
return fusedFrames;
|
|
}
|
|
|
|
TimeFrames SpotMarketManager::fusePartialTimeFrames(const TimeFrames &timeFrames)
|
|
{
|
|
TimeFrames fusedPartialFrames;
|
|
QList<int> handledIndices;
|
|
|
|
// If we have any partial hours left which could not be extended yet, try to extend with any other partially unfused hour
|
|
for (int i = 0; i < timeFrames.count(); i++) {
|
|
|
|
if (handledIndices.contains(i))
|
|
continue;
|
|
|
|
const TimeFrame currentFrame = timeFrames.at(i);
|
|
TimeFrame currentFrameMovedToEnd;
|
|
if (currentFrame.endDateTime().time().minute() == 0) {
|
|
// This time frame is already at the end. No need to move
|
|
currentFrameMovedToEnd = currentFrame;
|
|
} else {
|
|
int endtimeOffset = 60 - currentFrame.endDateTime().time().minute();
|
|
QDateTime endDateTime = currentFrame.endDateTime().addSecs(endtimeOffset * 60);
|
|
currentFrameMovedToEnd.setEndDateTime(endDateTime);
|
|
currentFrameMovedToEnd.setStartDateTime(endDateTime.addSecs(static_cast<int>(currentFrame.durationMinutes()) * -60));
|
|
}
|
|
|
|
|
|
// Check if we can extend an existing frame
|
|
bool extended = false;
|
|
|
|
// Try to fuse the start partial frame with any other partial frame
|
|
for (int j = 0; j < timeFrames.count(); j++) {
|
|
|
|
// We don't want to fuse with our selfs or already fused frames
|
|
if (j == i || handledIndices.contains(j))
|
|
continue;
|
|
|
|
if (currentFrame.endDateTime() == timeFrames.at(j).startDateTime()) {
|
|
// Prepend partial hour
|
|
TimeFrame extendedFrame = timeFrames.at(j);
|
|
extendedFrame.setStartDateTime(currentFrame.startDateTime());
|
|
fusedPartialFrames.append(extendedFrame);
|
|
handledIndices << j << i;
|
|
extended = true;
|
|
break;
|
|
} else if (currentFrame.startDateTime() == timeFrames.at(j).endDateTime()) {
|
|
// Append partial hour
|
|
TimeFrame extendedFrame = timeFrames.at(j);
|
|
extendedFrame.setEndDateTime(currentFrame.endDateTime());
|
|
fusedPartialFrames.append(extendedFrame);
|
|
handledIndices << j << i;
|
|
extended = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!extended && currentFrame != currentFrameMovedToEnd) {
|
|
// Try to fuse the end partial frame
|
|
for (int j = 0; j < timeFrames.count(); j++) {
|
|
|
|
// We don't want to fuse with our selfs
|
|
if (j == i || handledIndices.contains(j))
|
|
continue;
|
|
|
|
if (currentFrameMovedToEnd.endDateTime() == timeFrames.at(j).startDateTime()) {
|
|
// Prepend partial hour
|
|
TimeFrame extendedFrame = timeFrames.at(j);
|
|
extendedFrame.setStartDateTime(currentFrameMovedToEnd.startDateTime());
|
|
fusedPartialFrames.append(extendedFrame);
|
|
handledIndices << j << i;
|
|
extended = true;
|
|
break;
|
|
} else if (currentFrameMovedToEnd.startDateTime() == timeFrames.at(j).endDateTime()) {
|
|
// Append partial hour
|
|
TimeFrame extendedFrame = timeFrames.at(j);
|
|
extendedFrame.setEndDateTime(currentFrameMovedToEnd.endDateTime());
|
|
fusedPartialFrames.append(extendedFrame);
|
|
handledIndices << j << i;
|
|
extended = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!extended) {
|
|
// Note: we have a standalone partial hour...add it as is,
|
|
// it might overlap into an other hour or bet the current,
|
|
// we don't know here, so let's not change it. Do not use
|
|
// the moved to end frame since that destroyes the upper logic.
|
|
fusedPartialFrames.append(currentFrame);
|
|
handledIndices << i;
|
|
continue;
|
|
}
|
|
}
|
|
|
|
// Note: these frames must all be partial, we fuse them if possible, otherwise return them as single partial slots...
|
|
|
|
return fusedPartialFrames;
|
|
}
|
|
|
|
void SpotMarketManager::onProviderScoreEntriesChanged(const ScoreEntries &scoreEntries)
|
|
{
|
|
// Received new score entries, check if we habe to update the day based weighted scores.
|
|
QHash<QDate, ScoreEntries> weightedScores;
|
|
foreach (const ScoreEntry &scoreEntry, scoreEntries) {
|
|
if (!weightedScores.contains(scoreEntry.startDateTime().date())) {
|
|
// New day, new situation... let's fill in the data and weight them
|
|
weightedScores[scoreEntry.startDateTime().date()] = ScoreEntries({ scoreEntry });
|
|
} else {
|
|
weightedScores[scoreEntry.startDateTime().date()].append(scoreEntry);
|
|
}
|
|
}
|
|
|
|
// Insert all received scored weighted into our peristant hash
|
|
foreach (const QDate &date, weightedScores.keys()) {
|
|
if (!m_weightedScores.contains(date)) {
|
|
m_weightedScores[date] = weightScoreEntries(weightedScores.value(date));
|
|
}
|
|
}
|
|
|
|
// Clean up old
|
|
QList<QDate> datesToRemove;
|
|
foreach (const QDate &date, m_weightedScores.keys()) {
|
|
if (date < scoreEntries.first().startDateTime().date()) {
|
|
datesToRemove.append(date);
|
|
continue;
|
|
}
|
|
|
|
// Weight the current data
|
|
//m_weightedScores[date] = weightScoreEntries(m_weightedScores.value(date));
|
|
}
|
|
|
|
// Remove the past... live goes on
|
|
foreach (const QDate &date, datesToRemove) {
|
|
m_weightedScores.remove(date);
|
|
}
|
|
|
|
qCDebug(dcNymeaEnergy()) << "SpotMarketManager: Score entries updated";
|
|
emit scoreEntriesUpdated();
|
|
}
|
|
|