arbitre : borne ajoutée d'office même au nom trop long, libellé coupé à 256 caractères (Q7)
Écart « important » de la relecture d'etm72. provisionEvChargerConfig() prenait le nom du Thing pour libellé et réécrivait l'ensemble complet : un nom de plus de 256 caractères, ou un libellé hérité trop long sur une AUTRE entrée, refusait l'ajout, et la borne restait arbitrée sans rang réglable. - Le libellé est le nom coupé à 256 points de code (LoadConfigStore:: leftChars), jamais au milieu d'une paire de substitution. - L'ajout passe par appendConfig() : les bornes de saisie ne portent que sur l'entrée apportée. L'écriture cliente, elle, reste refusée tant que l'hérité n'est pas raccourci (règle d'etm72 inchangée). - Le journal nomme la borne par ce libellé coupé. Preuve : testAutoProvisionedLabelIsCutNeverRefused (nom de 300 caractères, emoji en 256e position, hérité de 300 caractères sur une autre entrée). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
f7aa370acb
commit
a728a3c8da
@ -1662,7 +1662,7 @@ void EnergyArbitrator::provisionEvChargerConfig(EvCharger *ev)
|
|||||||
return;
|
return;
|
||||||
|
|
||||||
const QString id = ev->thing()->id().toString();
|
const QString id = ev->thing()->id().toString();
|
||||||
LoadConfigs configs = m_loadConfigStore->configs();
|
const LoadConfigs configs = m_loadConfigStore->configs();
|
||||||
|
|
||||||
// LM-1209 — EN QUEUE, jamais en tête. L'auto-provisionnement REMPLIT UN TROU : sans entrée,
|
// LM-1209 — EN QUEUE, jamais en tête. L'auto-provisionnement REMPLIT UN TROU : sans entrée,
|
||||||
// la borne serait arbitrée sans rang réglable. Il ne s'ensuit rien sur sa PLACE — le rang
|
// la borne serait arbitrée sans rang réglable. Il ne s'ensuit rien sur sa PLACE — le rang
|
||||||
@ -1688,7 +1688,10 @@ void EnergyArbitrator::provisionEvChargerConfig(EvCharger *ev)
|
|||||||
|
|
||||||
LoadConfig c;
|
LoadConfig c;
|
||||||
c.setId(id);
|
c.setId(id);
|
||||||
c.setLabel(ev->name());
|
// Q7 — le libellé est le nom du Thing, COUPÉ à la borne de saisie (en caractères, jamais au
|
||||||
|
// milieu d'une paire de substitution). Un nom plus long n'est pas une raison de laisser la
|
||||||
|
// borne sans rang réglable : le libellé n'est qu'un nom, l'installateur le renomme.
|
||||||
|
c.setLabel(LoadConfigStore::leftChars(ev->name(), LoadConfigStore::MaxLabelChars));
|
||||||
c.setAdapter(QStringLiteral("evcharger"));
|
c.setAdapter(QStringLiteral("evcharger"));
|
||||||
c.setMode(QStringLiteral("dynamic"));
|
c.setMode(QStringLiteral("dynamic"));
|
||||||
// Le domaine d'une borne n'est pas une devinette : c'est le mécanisme lui-même. La réserve
|
// Le domaine d'une borne n'est pas une devinette : c'est le mécanisme lui-même. La réserve
|
||||||
@ -1704,10 +1707,12 @@ void EnergyArbitrator::provisionEvChargerConfig(EvCharger *ev)
|
|||||||
// rang est un choix » — un client ne peut pas le deviner, rien dans le nombre ne le dit.
|
// rang est un choix » — un client ne peut pas le deviner, rien dans le nombre ne le dit.
|
||||||
c.setRankOrigin(QStringLiteral("auto"));
|
c.setRankOrigin(QStringLiteral("auto"));
|
||||||
c.setEnabled(true);
|
c.setEnabled(true);
|
||||||
configs.append(c);
|
|
||||||
|
|
||||||
|
// appendConfig() et non setConfigs() : les bornes de saisie ne portent que sur l'entrée
|
||||||
|
// AJOUTÉE. Un libellé hérité trop long sur une AUTRE charge refuserait sinon l'ajout — le
|
||||||
|
// moteur ne sait pas le réparer, et c'est l'installateur qu'il attend (relecture etm72).
|
||||||
QString erreur;
|
QString erreur;
|
||||||
if (!m_loadConfigStore->setConfigs(configs, &erreur)) {
|
if (!m_loadConfigStore->appendConfig(c, &erreur)) {
|
||||||
// 7-c — l'échec ne doit pas être plus discret que le succès : sans entrée, la borne
|
// 7-c — l'échec ne doit pas être plus discret que le succès : sans entrée, la borne
|
||||||
// reste arbitrée mais absente de GetLoadConfig, et c'est précisément le trou qu'on
|
// reste arbitrée mais absente de GetLoadConfig, et c'est précisément le trou qu'on
|
||||||
// vient refermer.
|
// vient refermer.
|
||||||
@ -1720,7 +1725,7 @@ void EnergyArbitrator::provisionEvChargerConfig(EvCharger *ev)
|
|||||||
if (!m_provisionRefuse.contains(id)) {
|
if (!m_provisionRefuse.contains(id)) {
|
||||||
m_provisionRefuse.insert(id);
|
m_provisionRefuse.insert(id);
|
||||||
qCWarning(dcNymeaEnergy()) << "[EnergyArbitrator] Configuration automatique de la borne"
|
qCWarning(dcNymeaEnergy()) << "[EnergyArbitrator] Configuration automatique de la borne"
|
||||||
<< ev->name() << "REFUSÉE :" << erreur
|
<< c.label() << "REFUSÉE :" << erreur
|
||||||
<< "— elle sera arbitrée sans rang configurable. Réessayé"
|
<< "— elle sera arbitrée sans rang configurable. Réessayé"
|
||||||
<< "à chaque cycle ; cet avertissement ne sera pas répété.";
|
<< "à chaque cycle ; cet avertissement ne sera pas répété.";
|
||||||
}
|
}
|
||||||
|
|||||||
@ -6250,6 +6250,83 @@ void Simulation::testSetLoadConfigRefusalIsPublished()
|
|||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void Simulation::testAutoProvisionedLabelIsCutNeverRefused()
|
||||||
|
{
|
||||||
|
#ifndef ETM_ARBITRATOR
|
||||||
|
QSKIP("testAutoProvisionedLabelIsCutNeverRefused nécessite ETM_ARBITRATOR.");
|
||||||
|
#else
|
||||||
|
// [Q7, suite — relecture etm72, écart « important »] La borne prenait le nom de son Thing pour
|
||||||
|
// libellé et passait par l'écriture complète : un nom de plus de 256 caractères, ou un libellé
|
||||||
|
// hérité trop long sur une AUTRE entrée, refusait l'ajout, et la borne restait arbitrée sans
|
||||||
|
// rang réglable. Le nom est désormais coupé, et l'ajout ne borne que ce qu'il apporte.
|
||||||
|
const QString cfgPath = QDir::tempPath() + "/etm-loadcfg-nomlong.json";
|
||||||
|
QFile::remove(cfgPath);
|
||||||
|
qputenv("NYMEA_ENERGY_LOAD_CONFIG", cfgPath.toUtf8());
|
||||||
|
const auto nettoyer = qScopeGuard([&] {
|
||||||
|
qunsetenv("NYMEA_ENERGY_LOAD_CONFIG");
|
||||||
|
QFile::remove(cfgPath);
|
||||||
|
});
|
||||||
|
// Une configuration HÉRITÉE dont une autre entrée porte un libellé de 300 caractères.
|
||||||
|
const QString libelleHerite(300, QLatin1Char('L'));
|
||||||
|
{
|
||||||
|
QFile f(cfgPath);
|
||||||
|
QVERIFY(f.open(QIODevice::WriteOnly | QIODevice::Text));
|
||||||
|
f.write(QJsonDocument(QJsonObject{{"version", 1}, {"loads", QJsonArray{QJsonObject::fromVariantMap({
|
||||||
|
{"id", "ballon-herite"}, {"label", libelleHerite}, {"adapter", "relay-router"}, {"mode", "fixed"},
|
||||||
|
{"priority", 1}, {"enabled", false}, {"minOnS", 0}, {"minOffS", 0},
|
||||||
|
{"relays", QVariantList{QVariantMap{{"thingId", "{aaaaaaaa-0000-4000-8000-0000000000e1}"},
|
||||||
|
{"powerW", 1500}}}}})}}}).toJson());
|
||||||
|
}
|
||||||
|
cleanupTestCase();
|
||||||
|
m_energyLogDbFilePath = ":/databases/2022-06-22-energylogs.sqlite";
|
||||||
|
initTestCase();
|
||||||
|
|
||||||
|
EnergyArbitrator *arb = dynamic_cast<EnergyArbitrator *>(m_experiencePlugin->smartChargingManager());
|
||||||
|
QVERIFY(arb);
|
||||||
|
ThingManager *tm = NymeaCore::instance()->thingManager();
|
||||||
|
QUuid meterId = addMeter();
|
||||||
|
m_experiencePlugin->energyManager()->setRootMeter(meterId);
|
||||||
|
Thing *meter = tm->findConfiguredThing(meterId);
|
||||||
|
QVERIFY(meter);
|
||||||
|
meter->setStateValue("connected", true);
|
||||||
|
meter->setStateValue("currentPower", -4000);
|
||||||
|
|
||||||
|
QUuid borneId = addCharger("A", 16, 26657);
|
||||||
|
Thing *borne = tm->findConfiguredThing(borneId);
|
||||||
|
QVERIFY(borne);
|
||||||
|
// 300 caractères, un emoji en 256e position : couper en unités UTF-16 le briserait en deux.
|
||||||
|
const QString prise = QString::fromUcs4(U"\U0001F50C");
|
||||||
|
const QString nom = QString(255, QLatin1Char('a')) + prise + QString(44, QLatin1Char('b'));
|
||||||
|
QCOMPARE(nom.toUcs4().size(), qsizetype(300));
|
||||||
|
borne->setName(nom);
|
||||||
|
armerBorne(borne);
|
||||||
|
|
||||||
|
arb->simulationCallUpdate(utcDateTime(QDate(2026, 6, 8), QTime(13, 0, 0)));
|
||||||
|
QCoreApplication::processEvents();
|
||||||
|
|
||||||
|
const QHash<QString, QVariantMap> cfg = configsParId(injectAndWait("NymeaEnergy.GetLoadConfig"));
|
||||||
|
QVERIFY2(cfg.contains(borneId.toString()), "la borne au nom de 300 caractères n'a pas été ajoutée");
|
||||||
|
const QString libelle = cfg.value(borneId.toString()).value("label").toString();
|
||||||
|
QCOMPARE(libelle, QString(255, QLatin1Char('a')) + prise); // la paire est ENTIÈRE
|
||||||
|
QCOMPARE(libelle.toUcs4().size(), qsizetype(LoadConfigStore::MaxLabelChars));
|
||||||
|
QCOMPARE(cfg.value(borneId.toString()).value("rankOrigin").toString(), QStringLiteral("auto"));
|
||||||
|
QCOMPARE(cfg.value(borneId.toString()).value("priority").toInt(), 2); // en queue
|
||||||
|
// L'entrée héritée est intacte : le moteur n'a rien « réparé » d'office.
|
||||||
|
QCOMPARE(cfg.value("ballon-herite").value("label").toString(), libelleHerite);
|
||||||
|
|
||||||
|
// CONTRE-ÉPREUVE en place : le libellé hérité dépasse BIEN la borne — l'écho client de ce
|
||||||
|
// même ensemble est refusé, sur lui. C'est l'ajout du moteur qui en est exempté, pas l'hérité
|
||||||
|
// qui serait passé inaperçu.
|
||||||
|
const QVariantList ensemble = injectAndWait("NymeaEnergy.GetLoadConfig").toMap()
|
||||||
|
.value("params").toMap().value("loadConfigs").toList();
|
||||||
|
const QVariantMap p = injectAndWait("NymeaEnergy.SetLoadConfig", {{"loadConfigs", ensemble}})
|
||||||
|
.toMap().value("params").toMap();
|
||||||
|
QCOMPARE(p.value("energyError").toString(), QStringLiteral("EnergyErrorInvalidParameter"));
|
||||||
|
QCOMPARE(p.value("faultParams").toMap().value("cause").toString(), QStringLiteral("LABEL"));
|
||||||
|
QCOMPARE(p.value("faultParams").toMap().value("id").toString(), QStringLiteral("ballon-herite"));
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
void Simulation::testVanishedRelayFreezesTheLoad()
|
void Simulation::testVanishedRelayFreezesTheLoad()
|
||||||
{
|
{
|
||||||
#ifndef ETM_ARBITRATOR
|
#ifndef ETM_ARBITRATOR
|
||||||
|
|||||||
@ -254,6 +254,11 @@ private slots:
|
|||||||
// faultParams), déclarée dans le schéma, sans rien modifier — mémoire, GetLoadConfig, disque.
|
// faultParams), déclarée dans le schéma, sans rien modifier — mémoire, GetLoadConfig, disque.
|
||||||
void testSetLoadConfigRefusalIsPublished();
|
void testSetLoadConfigRefusalIsPublished();
|
||||||
|
|
||||||
|
// [Q7, suite — relecture etm72] Une borne au nom de 300 caractères est AJOUTÉE, son libellé
|
||||||
|
// coupé à 256 caractères sans couper de paire de substitution ; un libellé hérité trop long
|
||||||
|
// sur une autre entrée ne bloque pas l'ajout.
|
||||||
|
void testAutoProvisionedLabelIsCutNeverRefused();
|
||||||
|
|
||||||
// [ECS-111] Un relais DÉCLARÉ qui disparaît du registre nymea : la charge sort de
|
// [ECS-111] Un relais DÉCLARÉ qui disparaît du registre nymea : la charge sort de
|
||||||
// l'arbitrage (available faux, THING_MISSING) et reste dans la comptabilité, figée à la
|
// l'arbitrage (available faux, THING_MISSING) et reste dans la comptabilité, figée à la
|
||||||
// puissance crue engagée. Avant, elle publiait le nominal du palier entier — part du
|
// puissance crue engagée. Avant, elle publiait le nominal du palier entier — part du
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user