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;
|
||||
|
||||
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,
|
||||
// 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;
|
||||
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.setMode(QStringLiteral("dynamic"));
|
||||
// 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.
|
||||
c.setRankOrigin(QStringLiteral("auto"));
|
||||
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;
|
||||
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
|
||||
// reste arbitrée mais absente de GetLoadConfig, et c'est précisément le trou qu'on
|
||||
// vient refermer.
|
||||
@ -1720,7 +1725,7 @@ void EnergyArbitrator::provisionEvChargerConfig(EvCharger *ev)
|
||||
if (!m_provisionRefuse.contains(id)) {
|
||||
m_provisionRefuse.insert(id);
|
||||
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é"
|
||||
<< "à chaque cycle ; cet avertissement ne sera pas répété.";
|
||||
}
|
||||
|
||||
@ -6250,6 +6250,83 @@ void Simulation::testSetLoadConfigRefusalIsPublished()
|
||||
#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()
|
||||
{
|
||||
#ifndef ETM_ARBITRATOR
|
||||
|
||||
@ -254,6 +254,11 @@ private slots:
|
||||
// faultParams), déclarée dans le schéma, sans rien modifier — mémoire, GetLoadConfig, disque.
|
||||
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
|
||||
// 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
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user