Patrick Schurig 66c81086a9 feat(base): BaseloadLog — minutes du bilan, profil médian, qualité dite
Brief 3 T4. Une minute = consommation du site (échantillon d'une minute du
bilan, jamais ses cumuls) moins chaque charge pilotée selon sa source ; sous
none, sa puissance commandée, et la minute le garde. Deux tables dédiées
dans energy-forecast.sqlite (baseloadMinute, baseloadMinuteLoad), fil
d'écriture propre, lots de 5 min, lecture par journée, rétention 35 jours.
forecastPv et forecastLearning ne sont pas touchées.

Profil : médiane par case locale et type de jour (ouvré/week-end) sur
28 jours ; none sous 7 jours couverts à 90 %. Qualité : minutes comptées à
la commande, négatives, écartées, et par charge source et
constantMeasurement (la constante déguisée du banc).

Tests loadmodel : soustraction par source, profil synthétique (médiane,
jours de 23/25 h), seuil des 7 jours, fichier etm67 réel intact, jamais
bloquant, redémarrage, rétention, fichier illisible.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 09:51:23 +02:00

557 lines
23 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// SPDX-License-Identifier: GPL-3.0-or-later
// Copyright (C) 2025 - 2026, Patrick Schurig / ETM PowerSync
#include "baseloadlog.h"
#include <QDir>
#include <QFileInfo>
#include <QLoggingCategory>
#include <QMap>
#include <QMutexLocker>
#include <QSqlDatabase>
#include <QSqlError>
#include <QSqlQuery>
#include <QThread>
#include <QUuid>
#include <QtMath>
#include <algorithm>
Q_DECLARE_LOGGING_CATEGORY(dcNymeaEnergy)
namespace {
//! Les colonnes, dans l'ordre : elles servent à CRÉER et à RECONNAÎTRE une table.
const QStringList ColonnesMinute = {
QStringLiteral("at"), QStringLiteral("siteW"), QStringLiteral("loadsW"), QStringLiteral("baseW"),
QStringLiteral("commandedLoads"), QStringLiteral("writtenAt")
};
const QStringList ColonnesCharge = {
QStringLiteral("at"), QStringLiteral("loadId"), QStringLiteral("source"), QStringLiteral("powerW")
};
const char *CreationMinute =
"CREATE TABLE IF NOT EXISTS baseloadMinute ("
// FIN de la minute, epoch s UTC — l'horodatage de l'échantillon du bilan, tel quel.
" at INTEGER NOT NULL PRIMARY KEY,"
// Consommation du SITE, W moyens : import − export + production − stockage (bilan 1 min).
" siteW REAL NOT NULL,"
// Σ des charges pilotées retenues ; NULL si l'une n'avait pas de puissance connue.
" loadsW REAL,"
// siteW − loadsW ; NULL avec loadsW. Une valeur NÉGATIVE est gardée : elle est un constat.
" baseW REAL,"
" commandedLoads INTEGER NOT NULL," // charges comptées à leur COMMANDE (none)
" writtenAt INTEGER NOT NULL)"; // ms
const char *CreationCharge =
"CREATE TABLE IF NOT EXISTS baseloadMinuteLoad ("
" at INTEGER NOT NULL,"
" loadId TEXT NOT NULL,"
" source TEXT NOT NULL," // meter | device | none
" powerW REAL," // mesuré, ou commandé sous none ; NULL inconnu
" PRIMARY KEY (at, loadId))";
QVariant nul(QMetaType::Type t) { return QVariant(QMetaType(t)); }
std::optional<double> mediane(QList<double> v)
{
if (v.isEmpty())
return std::nullopt;
std::sort(v.begin(), v.end());
const int n = v.count();
return n % 2 ? v.at(n / 2) : (v.at(n / 2 - 1) + v.at(n / 2)) / 2.0;
}
//! Type de jour : 0 ouvré (lundi-vendredi), 1 week-end. Les jours fériés ne sont pas connus.
int typeDeJour(const QDate &d) { return d.dayOfWeek() >= 6 ? 1 : 0; }
}
// ============================================================================================
// La minute, et le profil — logique PURE
// ============================================================================================
std::optional<double> BaseloadMinute::baseW() const
{
double somme = 0;
for (const BaseloadLoadPart &c : charges) {
if (!c.powerW)
return std::nullopt; // une commande inconnue : on ne compte pas 0
somme += *c.powerW;
}
return siteW - somme;
}
int BaseloadMinute::chargesCommandees() const
{
int n = 0;
for (const BaseloadLoadPart &c : charges)
n += c.source == QLatin1String("none") ? 1 : 0;
return n;
}
BaseloadProfile BaseloadLog::calculerProfil(const QList<BaseloadMinute> &minutes, const QDateTime &now,
const QTimeZone &fuseau, int resolutionS)
{
BaseloadProfile p;
p.calcule = true;
p.calculeA = now.toUTC();
p.resolutionS = resolutionS;
const int cases = resolutionS > 0 ? 86400 / resolutionS : 0;
p.medianes[0] = QVector<std::optional<double>>(cases);
p.medianes[1] = QVector<std::optional<double>>(cases);
const qint64 finS = now.toSecsSinceEpoch();
const qint64 debutS = finS - qint64(BaseloadProfile::FenetreJours) * 86400;
// (jour local, case) → somme et nombre de minutes ; jour local → minutes connues.
struct Cumul { double somme = 0; int n = 0; };
QMap<QPair<QDate, int>, Cumul> parCase;
QMap<QDate, int> parJour;
struct ParCharge { QString derniereSource; qint64 derniere = 0; QHash<QString, int> parSource;
double min = 0, max = 0; int mesurees = 0; };
QMap<QString, ParCharge> charges;
for (const BaseloadMinute &m : minutes) {
if (m.finMinute <= debutS || m.finMinute > finS)
continue;
// La minute couvre [fin − 60 s, fin) : elle se range sous l'heure murale de son DÉBUT.
const QDateTime locale = QDateTime::fromSecsSinceEpoch(m.finMinute - 60, QTimeZone::utc())
.toTimeZone(fuseau);
for (const BaseloadLoadPart &c : m.charges) {
ParCharge &q = charges[c.loadId];
q.parSource[c.source] += 1;
if (m.finMinute >= q.derniere) {
q.derniere = m.finMinute;
q.derniereSource = c.source;
}
if (c.source != QLatin1String("none") && c.powerW) {
q.min = q.mesurees == 0 ? *c.powerW : qMin(q.min, *c.powerW);
q.max = q.mesurees == 0 ? *c.powerW : qMax(q.max, *c.powerW);
++q.mesurees;
}
}
const std::optional<double> b = m.baseW();
if (!b) {
++p.minutesIncompletes;
continue;
}
++p.minutes;
if (m.chargesCommandees() > 0)
++p.minutesEstimees;
if (*b < 0)
++p.minutesNegatives;
if (cases == 0)
continue;
const int c = (locale.time().msecsSinceStartOfDay() / 1000) / resolutionS;
Cumul &cu = parCase[qMakePair(locale.date(), c)];
cu.somme += *b;
++cu.n;
parJour[locale.date()] += 1;
}
// UN JOUR COMPTE s'il est couvert à 90 % de SES minutes — 1 380, 1 440 ou 1 500 selon
// l'heure d'été : la durée du jour local se lit en instants, jamais en heures murales.
for (auto it = parJour.constBegin(); it != parJour.constEnd(); ++it) {
const QDateTime d0(it.key(), QTime(0, 0), fuseau);
const QDateTime d1(it.key().addDays(1), QTime(0, 0), fuseau);
const qint64 attendues = (d1.toSecsSinceEpoch() - d0.toSecsSinceEpoch()) / 60;
if (it.value() >= qCeil(BaseloadProfile::CouvertureMin * attendues))
(typeDeJour(it.key()) == 0 ? p.joursOuvres : p.joursWeekEnd) += 1;
}
// LA MÉDIANE, par type de jour et par case, des moyennes de case couvertes à 90 %.
QVector<QList<double>> valeurs[2] = {QVector<QList<double>>(cases), QVector<QList<double>>(cases)};
const int attenduesParCase = resolutionS / 60;
for (auto it = parCase.constBegin(); it != parCase.constEnd(); ++it) {
// Au jour de 25 h, la case répétée reçoit deux heures de minutes : sa moyenne les mêle,
// sa couverture est jugée sur une case normale. Au jour de 23 h, la case qui manque
// n'existe pas — elle ne compte pour rien.
if (it.value().n < qCeil(BaseloadProfile::CouvertureMin * attenduesParCase))
continue;
valeurs[typeDeJour(it.key().first)][it.key().second] << it.value().somme / it.value().n;
}
const bool assez = p.joursOuvres + p.joursWeekEnd >= BaseloadProfile::JoursMin;
p.model = assez ? QStringLiteral("profile") : QStringLiteral("none");
if (assez) {
for (int t = 0; t < 2; ++t)
for (int c = 0; c < cases; ++c)
if (valeurs[t][c].count() >= BaseloadProfile::ValeursMinParCase)
p.medianes[t][c] = mediane(valeurs[t][c]);
}
for (auto it = charges.constBegin(); it != charges.constEnd(); ++it) {
BaseloadLoadQuality q;
q.loadId = it.key();
q.source = it.value().derniereSource;
q.minutesParSource = it.value().parSource;
if (it.value().mesurees >= BaseloadProfile::MinutesMesureesMin)
q.constante = (it.value().max - it.value().min) < BaseloadProfile::ConstanteW;
p.charges << q;
}
return p;
}
QList<std::optional<double>> BaseloadProfile::serie(const QDateTime &start, int stepS, int steps,
const QTimeZone &fuseau) const
{
QList<std::optional<double>> s;
for (int i = 0; i < steps; ++i)
s << std::nullopt;
if (model != QLatin1String("profile") || stepS != resolutionS || resolutionS <= 0)
return s;
for (int i = 0; i < steps; ++i) {
const QDateTime locale = start.addSecs(qint64(i) * stepS).toTimeZone(fuseau);
const int c = (locale.time().msecsSinceStartOfDay() / 1000) / resolutionS;
const QVector<std::optional<double>> &m = medianes[typeDeJour(locale.date())];
if (c >= 0 && c < m.count())
s[i] = m.at(c);
}
return s;
}
QVariantMap BaseloadProfile::qualiteVariant() const
{
QVariantMap q;
q.insert(QStringLiteral("windowDays"), FenetreJours);
q.insert(QStringLiteral("minDays"), JoursMin);
q.insert(QStringLiteral("resolutionS"), resolutionS);
q.insert(QStringLiteral("daysCovered"), QVariantMap{{QStringLiteral("workday"), joursOuvres},
{QStringLiteral("weekend"), joursWeekEnd}});
q.insert(QStringLiteral("minutes"), minutes);
q.insert(QStringLiteral("commandedMinutes"), minutesEstimees);
q.insert(QStringLiteral("negativeMinutes"), minutesNegatives);
q.insert(QStringLiteral("incompleteMinutes"), minutesIncompletes);
if (calculeA.isValid())
q.insert(QStringLiteral("computedAt"), calculeA.toUTC().toString(Qt::ISODate));
QVariantList cs;
for (const BaseloadLoadQuality &c : charges) {
QVariantMap m;
m.insert(QStringLiteral("loadId"), c.loadId);
m.insert(QStringLiteral("source"), c.source);
QVariantMap parSource;
for (auto it = c.minutesParSource.constBegin(); it != c.minutesParSource.constEnd(); ++it)
parSource.insert(it.key(), it.value());
m.insert(QStringLiteral("minutesBySource"), parSource);
// ABSENT faute de minutes mesurées pour se prononcer — jamais « false » par défaut.
if (c.constante)
m.insert(QStringLiteral("constantMeasurement"), *c.constante);
cs << m;
}
q.insert(QStringLiteral("loads"), cs);
return q;
}
// ============================================================================================
// LE FIL D'ÉCRITURE — sa propre connexion
// ============================================================================================
/*!
* \brief Écrit les minutes et recalcule le profil, dans son propre fil et sa propre connexion.
* \note Interne à \c BaseloadLog.
*/
class BaseloadWriter : public QObject
{
public:
BaseloadWriter(const QString &chemin, BaseloadLog *log, const QTimeZone &fuseau)
: m_chemin(chemin), m_log(log), m_fuseau(fuseau) {}
//! Minutes tenues en mémoire avant écriture : une transaction toutes les cinq minutes au
//! lieu d'une par minute — moins de verrous tenus sur le fichier que le cycle partage.
static constexpr int LotMinutes = 5;
void ouvrir()
{
QDir().mkpath(QFileInfo(m_chemin).absolutePath());
m_connexion = QStringLiteral("etm-baseload-%1").arg(QUuid::createUuid().toString(QUuid::WithoutBraces));
m_db = QSqlDatabase::addDatabase(QStringLiteral("QSQLITE"), m_connexion);
m_db.setDatabaseName(m_chemin);
m_db.setConnectOptions(QStringLiteral("QSQLITE_BUSY_TIMEOUT=10000"));
if (!m_db.open()) {
horsService(m_db.lastError().text());
return;
}
const bool ok = assurerTable(QStringLiteral("baseloadMinute"), ColonnesMinute, CreationMinute)
&& assurerTable(QStringLiteral("baseloadMinuteLoad"), ColonnesCharge, CreationCharge);
m_log->m_valide.storeRelease(ok ? 1 : 0);
// UN PREMIER PROFIL dès l'ouverture : après un redémarrage, les 28 jours sont en base, et
// les attendre un quart d'heure laisserait le snapshot dire « none » sans raison.
if (ok)
calculer(QDateTime::currentDateTimeUtc(), 0);
}
void fermer()
{
vider();
if (m_db.isOpen())
m_db.close();
m_db = QSqlDatabase();
if (!m_connexion.isEmpty())
QSqlDatabase::removeDatabase(m_connexion);
}
void ajouter(const BaseloadMinute &m)
{
m_lot << m;
if (m_lot.count() >= LotMinutes)
vider();
// Le recalcul suit le temps des MINUTES, pas l'horloge murale : un test qui injecte une
// journée en une seconde la voit recalculée comme le banc la verrait.
if (m_dernierCalcul == 0 || m.finMinute - m_dernierCalcul >= BaseloadLog::RecalculS)
calculer(QDateTime::fromSecsSinceEpoch(m.finMinute, QTimeZone::utc()), 0);
}
void calculer(const QDateTime &now, int resolutionS)
{
vider();
if (resolutionS > 0)
m_resolution = resolutionS;
m_dernierCalcul = now.toSecsSinceEpoch();
if (!m_log->m_valide.loadAcquire()) {
m_log->m_calcules.fetchAndAddRelease(1);
return;
}
purger(now.toSecsSinceEpoch());
const qint64 fin = now.toSecsSinceEpoch();
const qint64 debut = fin - qint64(BaseloadProfile::FenetreJours) * 86400;
const QList<BaseloadMinute> minutes = lire(debut, fin);
const QTimeZone fuseau = m_fuseau.isValid() ? m_fuseau : QTimeZone::systemTimeZone();
const BaseloadProfile p = BaseloadLog::calculerProfil(minutes, now, fuseau, m_resolution);
m_log->publierProfil(p);
m_log->m_calcules.fetchAndAddRelease(1);
// 7-c — l'ISSUE à chaque calcul, et ce qui a été LU : combien de minutes, de quelle nature.
qCInfo(dcNymeaEnergy()).noquote()
<< QStringLiteral("[Base] profil %1 — %2 jour(s) ouvré(s) + %3 de week-end couverts "
"(seuil %4) ; %5 minute(s) connues dont %6 à la commande, %7 négative(s), "
"%8 écartée(s)")
.arg(p.model).arg(p.joursOuvres).arg(p.joursWeekEnd).arg(BaseloadProfile::JoursMin)
.arg(p.minutes).arg(p.minutesEstimees).arg(p.minutesNegatives)
.arg(p.minutesIncompletes);
}
private:
void vider()
{
if (m_lot.isEmpty() || !m_log->m_valide.loadAcquire()) {
m_lot.clear();
return;
}
if (!m_db.transaction()) {
qCWarning(dcNymeaEnergy()) << "[Base] écriture remise à plus tard —" << m_db.lastError().text();
return; // le lot reste en mémoire, on réessaiera
}
QSqlQuery qm(m_db), qc(m_db);
// `ON CONFLICT … DO NOTHING`, jamais `INSERT OR IGNORE` : celui-ci avalerait aussi une
// violation de NOT NULL, et une minute mal formée disparaîtrait sans trace (leçon T2).
qm.prepare(QStringLiteral("INSERT INTO baseloadMinute (%1) VALUES (?,?,?,?,?,?) "
"ON CONFLICT (at) DO NOTHING").arg(ColonnesMinute.join(',')));
qc.prepare(QStringLiteral("INSERT INTO baseloadMinuteLoad (%1) VALUES (?,?,?,?) "
"ON CONFLICT (at, loadId) DO NOTHING").arg(ColonnesCharge.join(',')));
const qint64 ecrit = QDateTime::currentMSecsSinceEpoch();
int inserees = 0;
for (const BaseloadMinute &m : std::as_const(m_lot)) {
const std::optional<double> b = m.baseW();
double charges = 0;
for (const BaseloadLoadPart &c : m.charges)
charges += c.powerW.value_or(0);
qm.addBindValue(m.finMinute);
qm.addBindValue(m.siteW);
qm.addBindValue(b ? QVariant(charges) : nul(QMetaType::Double));
qm.addBindValue(b ? QVariant(*b) : nul(QMetaType::Double));
qm.addBindValue(m.chargesCommandees());
qm.addBindValue(ecrit);
bool ok = qm.exec();
inserees += ok && qm.numRowsAffected() > 0 ? 1 : 0;
for (const BaseloadLoadPart &c : m.charges) {
if (!ok)
break;
qc.addBindValue(m.finMinute);
qc.addBindValue(c.loadId);
qc.addBindValue(c.source);
qc.addBindValue(c.powerW ? QVariant(*c.powerW) : nul(QMetaType::Double));
ok = qc.exec();
}
if (!ok) {
const QSqlError e = qm.lastError().isValid() ? qm.lastError() : qc.lastError();
m_db.rollback();
if (e.nativeErrorCode().toInt() == 5 || e.nativeErrorCode().toInt() == 6) {
qCWarning(dcNymeaEnergy()) << "[Base] base occupée —" << m_lot.count()
<< "minute(s) remise(s) au lot suivant";
return;
}
qCWarning(dcNymeaEnergy()) << "[Base]" << m_lot.count() << "minute(s) PERDUE(S) —"
<< e.text();
m_lot.clear();
return;
}
}
if (!m_db.commit()) {
m_db.rollback();
return;
}
m_log->m_ecrites.fetchAndAddRelease(inserees);
m_lot.clear();
}
/*!
* La fenêtre, lue PAR JOURNÉE : chaque requête est courte et rend le verrou du fichier
* entre deux. Une lecture de 28 jours d'un bloc tiendrait le verrou partagé assez longtemps
* pour faire attendre l'historique des révisions — qui écrit, lui, depuis le cycle.
*/
QList<BaseloadMinute> lire(qint64 debut, qint64 fin)
{
QList<BaseloadMinute> sortie;
QHash<qint64, int> index;
for (qint64 a = debut; a < fin; a += 86400) {
const qint64 b = qMin(fin, a + 86400);
QSqlQuery q(m_db);
q.prepare(QStringLiteral("SELECT at, siteW FROM baseloadMinute WHERE at > ? AND at <= ? ORDER BY at"));
q.addBindValue(a);
q.addBindValue(b);
if (q.exec()) {
while (q.next()) {
BaseloadMinute m;
m.finMinute = q.value(0).toLongLong();
m.siteW = q.value(1).toDouble();
index.insert(m.finMinute, sortie.count());
sortie << m;
}
}
q.finish();
QSqlQuery c(m_db);
c.prepare(QStringLiteral("SELECT at, loadId, source, powerW FROM baseloadMinuteLoad "
"WHERE at > ? AND at <= ?"));
c.addBindValue(a);
c.addBindValue(b);
if (c.exec()) {
while (c.next()) {
const int i = index.value(c.value(0).toLongLong(), -1);
if (i < 0)
continue;
BaseloadLoadPart part;
part.loadId = c.value(1).toString();
part.source = c.value(2).toString();
if (!c.value(3).isNull())
part.powerW = c.value(3).toDouble();
sortie[i].charges << part;
}
}
}
return sortie;
}
void purger(qint64 nowS)
{
const qint64 borne = nowS - qint64(BaseloadLog::RetentionJours) * 86400;
QSqlQuery q(m_db);
for (const char *table : {"baseloadMinute", "baseloadMinuteLoad"}) {
q.prepare(QStringLiteral("DELETE FROM %1 WHERE at <= ?").arg(QLatin1String(table)));
q.addBindValue(borne);
if (!q.exec())
qCWarning(dcNymeaEnergy()) << "[Base] purge refusée :" << q.lastError().text();
}
}
/*!
* La table existe-t-elle, avec la forme attendue ? Sinon elle est REFAITE — ce sont NOS
* tables, nées avec ce lot : aucune version antérieure n'en a écrit, il n'y a pas d'historique
* à migrer. Le jour où une colonne s'ajoutera, ce sera une migration en place, comme etm68.
* Les autres tables du fichier ne sont jamais touchées.
*/
bool assurerTable(const QString &nom, const QStringList &colonnes, const char *creation)
{
QSqlQuery q(m_db);
if (!q.exec(QStringLiteral("PRAGMA table_info(%1)").arg(nom))) {
horsService(q.lastError().text());
return false;
}
QStringList trouvees;
while (q.next())
trouvees << q.value(1).toString();
if (!trouvees.isEmpty() && trouvees != colonnes) {
qCWarning(dcNymeaEnergy()).noquote()
<< QStringLiteral("[Base] table %1 de forme inattendue (%2 colonne(s)) — REFAITE, "
"son contenu est perdu ; le service continue").arg(nom).arg(trouvees.count());
if (!q.exec(QStringLiteral("DROP TABLE IF EXISTS %1").arg(nom))) {
const QString cote = QStringLiteral("%1_abimee_%2").arg(nom).arg(QDateTime::currentMSecsSinceEpoch());
if (!q.exec(QStringLiteral("ALTER TABLE %1 RENAME TO %2").arg(nom, cote))) {
horsService(q.lastError().text());
return false;
}
}
}
if (!q.exec(QString::fromLatin1(creation))) {
horsService(q.lastError().text());
return false;
}
return true;
}
void horsService(const QString &erreur)
{
m_log->m_valide.storeRelease(0);
if (m_horsServiceSignale)
return;
m_horsServiceSignale = true;
qCWarning(dcNymeaEnergy()) << "[Base] consommation de base HORS SERVICE — base illisible"
<< m_chemin << ":" << erreur
<< "— rien n'est enregistré, l'arbitrage n'en dépend pas";
}
QString m_chemin;
BaseloadLog *m_log;
QTimeZone m_fuseau;
QString m_connexion;
QSqlDatabase m_db;
QList<BaseloadMinute> m_lot;
qint64 m_dernierCalcul = 0;
int m_resolution = BaseloadLog::ResolutionParDefautS;
bool m_horsServiceSignale = false;
};
// ============================================================================================
// LE CÔTÉ APPELANT — copies, aucune requête
// ============================================================================================
BaseloadLog::BaseloadLog(const QString &cheminFichier, const QTimeZone &fuseau, QObject *parent)
: QObject(parent)
{
m_valide.storeRelease(1);
m_fil = new QThread(this);
m_fil->setObjectName(QStringLiteral("etm-baseload"));
m_ecrivain = new BaseloadWriter(cheminFichier, this, fuseau);
m_ecrivain->moveToThread(m_fil);
m_fil->start(QThread::LowPriority);
BaseloadWriter *w = m_ecrivain;
QMetaObject::invokeMethod(w, [w]() { w->ouvrir(); }, Qt::QueuedConnection);
}
BaseloadLog::~BaseloadLog()
{
BaseloadWriter *w = m_ecrivain;
QMetaObject::invokeMethod(w, [w]() { w->fermer(); delete w; }, Qt::BlockingQueuedConnection);
m_fil->quit();
m_fil->wait();
}
void BaseloadLog::observerMinute(const BaseloadMinute &minute)
{
BaseloadWriter *w = m_ecrivain;
QMetaObject::invokeMethod(w, [w, minute]() { w->ajouter(minute); }, Qt::QueuedConnection);
}
void BaseloadLog::recalculer(const QDateTime &now, int resolutionS)
{
BaseloadWriter *w = m_ecrivain;
QMetaObject::invokeMethod(w, [w, now, resolutionS]() { w->calculer(now, resolutionS); },
Qt::QueuedConnection);
}
BaseloadProfile BaseloadLog::profil() const
{
QMutexLocker l(&m_verrou);
return m_profil;
}
void BaseloadLog::publierProfil(const BaseloadProfile &p)
{
QMutexLocker l(&m_verrou);
m_profil = p;
}