diff --git a/sunspec/froniusrules.cpp b/sunspec/froniusrules.cpp index d655b91..bd0bb50 100644 --- a/sunspec/froniusrules.cpp +++ b/sunspec/froniusrules.cpp @@ -40,6 +40,23 @@ SunSpecDataPoint::ByteOrder byteOrder() return SunSpecDataPoint::ByteOrderLittleEndian; } +MeterConnectionDecision decideMeterConnection(int meterSlaveId, int inverterSlaveId) +{ + if (meterSlaveId == 0) + return MeterConnectionDisabled; + + if (meterSlaveId < 0 || meterSlaveId > MaximumSlaveId) + return MeterConnectionInvalidSlaveId; + + /* Not a theoretical case: the slave ID of the thing is written by the + * discovery from the slave ID it was asked to scan, so an inverter found + * with a discovery slave ID of 200 ends up with both values at 200. */ + if (meterSlaveId == inverterSlaveId) + return MeterConnectionCollidesWithInverter; + + return MeterConnectionCreate; +} + int clampUpdateInterval(int requestedSeconds) { return qMax(requestedSeconds, MinimumUpdateIntervalSeconds); diff --git a/sunspec/froniusrules.h b/sunspec/froniusrules.h index ced9824..f98fd4d 100644 --- a/sunspec/froniusrules.h +++ b/sunspec/froniusrules.h @@ -63,6 +63,22 @@ const int DefaultRevertTimeSeconds = 300; * connection". */ const int InverterSlaveId = 1; const int DefaultMeterSlaveId = 200; +const int MaximumSlaveId = 247; + +/* Whether a second SunSpecConnection should be opened for the Smart Meter, and + * why not when it should not. The caller logs the reason; keeping it out of + * this function is what makes the decision testable without nymea. */ +enum MeterConnectionDecision { + MeterConnectionCreate, + /* 0 is the documented way of saying "no meter behind this inverter". */ + MeterConnectionDisabled, + /* A second socket on the unit ID already served by the inverter would + * rediscover the very same models and duplicate every child thing. */ + MeterConnectionCollidesWithInverter, + MeterConnectionInvalidSlaveId +}; + +MeterConnectionDecision decideMeterConnection(int meterSlaveId, int inverterSlaveId); bool matchesManufacturer(const QStringList &manufacturers); diff --git a/sunspec/tests/logic/tst_sunspec.cpp b/sunspec/tests/logic/tst_sunspec.cpp index fd218c8..04fe1c1 100644 --- a/sunspec/tests/logic/tst_sunspec.cpp +++ b/sunspec/tests/logic/tst_sunspec.cpp @@ -161,6 +161,14 @@ private slots: void test_AC_15_battery_power_is_positive_when_discharging(); void test_AC_15_battery_power_is_negative_when_charging(); + // F3 - multi unit ID + void test_F3_meter_slave_id_zero_disables_the_meter_connection(); + void test_F3_meter_slave_id_out_of_range_is_refused(); + void test_F3_meter_slave_id_equal_to_the_inverter_is_refused(); + void test_F3_meter_slave_id_creates_the_connection(); + void test_F3_meter_slave_id_is_not_marked_advanced(); + void test_F3_generic_vendors_have_no_meter_slave_id(); + private: QJsonObject m_pluginJson; @@ -782,6 +790,86 @@ void SunSpecFroniusTest::test_AC_15_battery_power_is_negative_when_charging() QCOMPARE(batteryPower(readings), -600.0); } +// --------------------------------------------------------------------------- +// F3 - multi unit ID +// +// Le GEN24 repond pour l'onduleur sur un unit ID et pour le Smart Meter sur un +// autre, derriere le meme host:port. La decision d'ouvrir ou non la seconde +// connexion est isolee ici pour etre testable sans nymea ; le plugin se contente +// de journaliser le motif du refus. +// --------------------------------------------------------------------------- + +void SunSpecFroniusTest::test_F3_meter_slave_id_zero_disables_the_meter_connection() +{ + using namespace FroniusRules; + + // 0 est la facon documentee de dire "pas de compteur derriere cet onduleur". + QCOMPARE(decideMeterConnection(0, InverterSlaveId), MeterConnectionDisabled); +} + +void SunSpecFroniusTest::test_F3_meter_slave_id_out_of_range_is_refused() +{ + using namespace FroniusRules; + + QCOMPARE(decideMeterConnection(248, InverterSlaveId), MeterConnectionInvalidSlaveId); + QCOMPARE(decideMeterConnection(-1, InverterSlaveId), MeterConnectionInvalidSlaveId); + + // La borne haute elle-meme reste valide. + QCOMPARE(decideMeterConnection(MaximumSlaveId, InverterSlaveId), MeterConnectionCreate); +} + +void SunSpecFroniusTest::test_F3_meter_slave_id_equal_to_the_inverter_is_refused() +{ + using namespace FroniusRules; + + // Une seconde socket sur l'unit ID deja servi par l'onduleur redecouvrirait + // exactement les memes modeles et dupliquerait chaque thing enfant. Le cas + // n'est pas theorique : la decouverte ecrit dans le parametre slaveId du + // thing l'unit ID qu'on lui a demande de scanner. + QCOMPARE(decideMeterConnection(1, 1), MeterConnectionCollidesWithInverter); + QCOMPARE(decideMeterConnection(200, 200), MeterConnectionCollidesWithInverter); +} + +void SunSpecFroniusTest::test_F3_meter_slave_id_creates_the_connection() +{ + using namespace FroniusRules; + + QCOMPARE(decideMeterConnection(DefaultMeterSlaveId, InverterSlaveId), MeterConnectionCreate); + QCOMPARE(DefaultMeterSlaveId, 200); + + // Compteurs secondaires. + QCOMPARE(decideMeterConnection(201, InverterSlaveId), MeterConnectionCreate); + QCOMPARE(decideMeterConnection(202, InverterSlaveId), MeterConnectionCreate); +} + +/* AC-7 demande meterSlaveId en "advanced". Cette propriete n'existe pas sur les + * paramTypes de nymea 1.15.2 : ParamType::typeProperties() ne la connait pas, et + * le verificateur de metadonnee rejette tout champ inconnu. Une metadonnee + * rejetee, ce n'est pas un parametre mal affiche, c'est un plugin qui ne se + * charge plus du tout. + * + * Ce test n'entérine pas l'ecart : il empeche qu'on le "corrige" sans mesurer. + * Voir sunspec/DIVERGENCES.md. */ +void SunSpecFroniusTest::test_F3_meter_slave_id_is_not_marked_advanced() +{ + const QJsonObject connection = thingClassByName("froniusConnection"); + const QJsonObject meterSlaveId = paramByName(connection, "meterSlaveId"); + QVERIFY(!meterSlaveId.isEmpty()); + + QVERIFY2(!meterSlaveId.contains("advanced"), + "\"advanced\" n'est pas une propriete de paramType connue de nymea 1.15.2 : " + "l'ajouter rend la metadonnee du plugin invalide et le plugin ne se charge plus. " + "Voir sunspec/DIVERGENCES.md."); +} + +void SunSpecFroniusTest::test_F3_generic_vendors_have_no_meter_slave_id() +{ + // Le multi unit ID est une particularite Fronius. Les vendors generique et + // solarEdge gardent exactement une connexion par thing. + QVERIFY(paramByName(thingClassByName("sunspecConnection"), "meterSlaveId").isEmpty()); + QVERIFY(paramByName(thingClassByName("solarEdgeConnection"), "meterSlaveId").isEmpty()); +} + QTEST_MAIN(SunSpecFroniusTest) #include "tst_sunspec.moc"