sunspec: add a device suite for the two connection behaviour
The two most certain defects of the multi unit ID work are invisible to the offline suites, because they only appear once a discovery has actually run: the orphan sweep deleting the children of the other connection, and the aggregation of the connected state. This suite mounts the plugin behind a real ThingManager against a real device. It is deliberately not a testcase, so "make check" never runs it, but it is still built so it cannot rot unnoticed. Without SUNSPEC_TEST_HOST every test skips, and no address is written into the repository: the address of a real installation is customer data. NOT YET RUN against hardware. It needs the address of a GEN24 and, given the polling rate guidance, a deliberate decision to point it at a customer inverter. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
e1e91a19c3
commit
9e0cd44af0
75
sunspec/tests/integration/integration.pro
Normal file
75
sunspec/tests/integration/integration.pro
Normal file
@ -0,0 +1,75 @@
|
||||
include(../../../sunspec.pri)
|
||||
|
||||
QT += testlib network serialbus
|
||||
QT -= gui
|
||||
TEMPLATE = app
|
||||
TARGET = tst_sunspecdevice
|
||||
|
||||
# Suite d'integration F3 : le plugin, monte derriere un vrai ThingManager, face
|
||||
# a un vrai appareil.
|
||||
#
|
||||
# VOLONTAIREMENT PAS "testcase" : "make check" ne la lance jamais. Elle est
|
||||
# quand meme construite, pour qu'elle ne pourrisse pas en silence.
|
||||
#
|
||||
# Elle exige un appareil joignable, passe par variable d'environnement :
|
||||
#
|
||||
# SUNSPEC_TEST_HOST=<adresse> [SUNSPEC_TEST_PORT=502]
|
||||
# [SUNSPEC_TEST_METER_SLAVE_ID=200] [SUNSPEC_TEST_SLAVE_ID=1]
|
||||
# ./tst_sunspecdevice
|
||||
#
|
||||
# Sans SUNSPEC_TEST_HOST, chaque test se met en QSKIP. Aucune adresse par
|
||||
# defaut n'est ecrite ici : l'adresse d'une installation reelle est une donnee
|
||||
# client et n'a rien a faire dans un depot a miroir public (voir AGENTS.md).
|
||||
#
|
||||
# ATTENTION avant de la lancer sur un onduleur client : le constructeur
|
||||
# recommande de rester au-dessus de 7 s d'intervalle d'interrogation, faute de
|
||||
# quoi l'onduleur peut passer hors ligne vis-a-vis de son portail, avec
|
||||
# retablissement par coupure AC+DC sur site. Cette suite ne fait qu'un cycle de
|
||||
# decouverte et n'interroge pas en boucle, mais elle ouvre bien deux connexions.
|
||||
|
||||
CONFIG += console c++17
|
||||
CONFIG -= app_bundle
|
||||
|
||||
CONFIG += link_pkgconfig
|
||||
PKGCONFIG += nymea nymea-core
|
||||
|
||||
PLUGIN_DIR = $$PWD/../..
|
||||
INCLUDEPATH += $$PLUGIN_DIR
|
||||
|
||||
DEFINES += SUNSPEC_PLUGIN_TEST_ACCESS
|
||||
|
||||
JSONFILE = $$PLUGIN_DIR/integrationpluginsunspec.json
|
||||
OTHER_FILES += $$JSONFILE
|
||||
|
||||
plugininfo.input = JSONFILE
|
||||
plugininfo.output = plugininfo.h
|
||||
plugininfo.commands = nymea-plugininfocompiler ${QMAKE_FILE_NAME} --output ${QMAKE_FILE_OUT}
|
||||
plugininfo.dependency_type = TYPE_C
|
||||
plugininfo.CONFIG += no_link combine
|
||||
QMAKE_EXTRA_COMPILERS += plugininfo
|
||||
|
||||
extern_plugininfo.input = JSONFILE
|
||||
extern_plugininfo.output = extern-plugininfo.h
|
||||
extern_plugininfo.commands = nymea-plugininfocompiler ${QMAKE_FILE_NAME} --extern ${QMAKE_FILE_OUT}
|
||||
extern_plugininfo.dependency_type = TYPE_C
|
||||
extern_plugininfo.CONFIG += no_link combine
|
||||
QMAKE_EXTRA_COMPILERS += extern_plugininfo
|
||||
|
||||
DEFINES += PLUGIN_JSON_PATH=\\\"$$JSONFILE\\\"
|
||||
|
||||
SOURCES += \
|
||||
tst_sunspecdevice.cpp \
|
||||
$$PLUGIN_DIR/integrationpluginsunspec.cpp \
|
||||
$$PLUGIN_DIR/froniusrules.cpp \
|
||||
$$PLUGIN_DIR/solaredgebattery.cpp \
|
||||
$$PLUGIN_DIR/sunspecdiscovery.cpp \
|
||||
$$PLUGIN_DIR/sunspecstoragelogic.cpp \
|
||||
$$PLUGIN_DIR/sunspecthing.cpp
|
||||
|
||||
HEADERS += \
|
||||
$$PLUGIN_DIR/integrationpluginsunspec.h \
|
||||
$$PLUGIN_DIR/froniusrules.h \
|
||||
$$PLUGIN_DIR/solaredgebattery.h \
|
||||
$$PLUGIN_DIR/sunspecdiscovery.h \
|
||||
$$PLUGIN_DIR/sunspecstoragelogic.h \
|
||||
$$PLUGIN_DIR/sunspecthing.h
|
||||
456
sunspec/tests/integration/tst_sunspecdevice.cpp
Normal file
456
sunspec/tests/integration/tst_sunspecdevice.cpp
Normal file
@ -0,0 +1,456 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
|
||||
*
|
||||
* Copyright (C) 2026, ETM-PowerSync
|
||||
*
|
||||
* This file is part of nymea.
|
||||
*
|
||||
* nymea is 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 is 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. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
|
||||
|
||||
/* F3 face a un vrai appareil.
|
||||
*
|
||||
* Les deux defauts les plus certains du multi unit ID sont invisibles pour les
|
||||
* suites hors ligne, parce qu'ils n'apparaissent qu'une fois la decouverte
|
||||
* reellement passee :
|
||||
*
|
||||
* - le balayage des orphelins, qui sur une seule des deux connexions ferait
|
||||
* disparaitre les enfants de l'autre a chaque decouverte ;
|
||||
* - l'agregation de l'etat connected entre les deux connexions.
|
||||
*
|
||||
* Cette suite n'est pas lancee par "make check" : elle exige un appareil. Voir
|
||||
* integration.pro pour les variables d'environnement. Sans SUNSPEC_TEST_HOST,
|
||||
* tout se met en QSKIP.
|
||||
*
|
||||
* Aucune adresse par defaut n'est ecrite ici. L'adresse d'une installation
|
||||
* reelle est une donnee client (AGENTS.md, "Miroir public").
|
||||
*/
|
||||
|
||||
#include <QtTest>
|
||||
#include <QCoreApplication>
|
||||
#include <QTemporaryDir>
|
||||
|
||||
#include <nymeasettings.h>
|
||||
#include <hardwaremanager.h>
|
||||
#include <plugintimer.h>
|
||||
#include <logging/logengine.h>
|
||||
#include <logging/logger.h>
|
||||
#include <network/networkdevicediscovery.h>
|
||||
#include <network/networkdevicemonitor.h>
|
||||
#include <integrations/thingmanagerimplementation.h>
|
||||
#include <integrations/thingsetupinfo.h>
|
||||
|
||||
#include <sunspecconnection.h>
|
||||
|
||||
#include "integrationpluginsunspec.h"
|
||||
|
||||
/* --------------------------------------------------------------------------
|
||||
* Doublures
|
||||
* -------------------------------------------------------------------------- */
|
||||
|
||||
/* Contrairement aux suites hors ligne, le moniteur porte ici une vraie adresse
|
||||
* et se declare joignable : c'est ce qui autorise le plugin a se connecter. */
|
||||
class DeviceMonitor : public NetworkDeviceMonitor
|
||||
{
|
||||
public:
|
||||
DeviceMonitor(const QHostAddress &address, QObject *parent = nullptr)
|
||||
: NetworkDeviceMonitor(parent), m_address(address)
|
||||
{
|
||||
m_info = NetworkDeviceInfo(address);
|
||||
m_info.setMonitorMode(NetworkDeviceInfo::MonitorModeIp);
|
||||
}
|
||||
|
||||
MacAddress macAddress() const override { return MacAddress(); }
|
||||
QString hostName() const override { return QString(); }
|
||||
QHostAddress address() const override { return m_address; }
|
||||
NetworkDeviceInfo::MonitorMode monitorMode() const override { return NetworkDeviceInfo::MonitorModeIp; }
|
||||
NetworkDeviceInfo networkDeviceInfo() const override { return m_info; }
|
||||
bool reachable() const override { return true; }
|
||||
QDateTime lastSeen() const override { return QDateTime::currentDateTime(); }
|
||||
uint pingRetries() const override { return 0; }
|
||||
void setPingRetries(uint) override {}
|
||||
|
||||
private:
|
||||
QHostAddress m_address;
|
||||
NetworkDeviceInfo m_info;
|
||||
};
|
||||
|
||||
class DeviceDiscovery : public NetworkDeviceDiscovery
|
||||
{
|
||||
public:
|
||||
DeviceDiscovery(const QHostAddress &address, QObject *parent = nullptr)
|
||||
: NetworkDeviceDiscovery(parent), m_address(address) {}
|
||||
|
||||
NetworkDeviceDiscoveryReply *discover() override { return nullptr; }
|
||||
bool running() const override { return false; }
|
||||
|
||||
NetworkDeviceMonitor *registerMonitor(Thing *) override { return new DeviceMonitor(m_address, this); }
|
||||
void unregisterMonitor(NetworkDeviceMonitor *monitor) override { delete monitor; }
|
||||
|
||||
PingReply *ping(const QHostAddress &, uint) override { return nullptr; }
|
||||
PingReply *ping(const QString &, uint) override { return nullptr; }
|
||||
MacAddressDatabaseReply *lookupMacAddress(const QString &) override { return nullptr; }
|
||||
MacAddressDatabaseReply *lookupMacAddress(const MacAddress &) override { return nullptr; }
|
||||
bool sendArpRequest(const QHostAddress &) override { return false; }
|
||||
NetworkDeviceInfos cache() const override { return NetworkDeviceInfos(); }
|
||||
|
||||
bool available() const override { return true; }
|
||||
bool enabled() const override { return true; }
|
||||
|
||||
protected:
|
||||
void setEnabled(bool) override {}
|
||||
|
||||
private:
|
||||
QHostAddress m_address;
|
||||
};
|
||||
|
||||
class NullPluginTimer : public PluginTimer
|
||||
{
|
||||
public:
|
||||
explicit NullPluginTimer(int seconds, QObject *parent = nullptr)
|
||||
: PluginTimer(parent), m_interval(seconds) {}
|
||||
|
||||
int interval() const override { return m_interval; }
|
||||
int currentTick() const override { return 0; }
|
||||
bool running() const override { return m_running; }
|
||||
|
||||
void reset() override {}
|
||||
void start() override { m_running = true; }
|
||||
void stop() override { m_running = false; }
|
||||
void pause() override {}
|
||||
void resume() override {}
|
||||
|
||||
private:
|
||||
int m_interval = 0;
|
||||
bool m_running = false;
|
||||
};
|
||||
|
||||
/* Le minuteur n'est jamais declenche par cette suite. Interroger en boucle un
|
||||
* onduleur client est precisement ce que le plancher de cadence cherche a
|
||||
* eviter ; ici on ne veut que la decouverte. */
|
||||
class NullPluginTimerManager : public PluginTimerManager
|
||||
{
|
||||
public:
|
||||
explicit NullPluginTimerManager(QObject *parent = nullptr) : PluginTimerManager(parent) {}
|
||||
|
||||
PluginTimer *registerTimer(int seconds) override { return new NullPluginTimer(seconds, this); }
|
||||
void unregisterTimer(PluginTimer *timer) override { delete timer; }
|
||||
|
||||
bool available() const override { return true; }
|
||||
bool enabled() const override { return true; }
|
||||
|
||||
protected:
|
||||
void setEnabled(bool) override {}
|
||||
};
|
||||
|
||||
/* Les things enfants sont crees par le plugin, pas ecrits d'avance dans les
|
||||
* reglages : on ne peut donc pas leur retirer leurs states journalises comme le
|
||||
* font les suites hors ligne. Il faut un LogEngine, meme creux, sans quoi le
|
||||
* ThingManager en dereference un absent des la creation du premier enfant. */
|
||||
class NullLogEngine : public LogEngine
|
||||
{
|
||||
public:
|
||||
explicit NullLogEngine(QObject *parent = nullptr) : LogEngine(parent) {}
|
||||
|
||||
Logger *registerLogSource(const QString &name, const QStringList &tags,
|
||||
Types::LoggingType loggingType, const QString &) override
|
||||
{
|
||||
Logger *logger = createLogger(name, tags, loggingType);
|
||||
m_loggers.insert(name, logger);
|
||||
return logger;
|
||||
}
|
||||
|
||||
void unregisterLogSource(const QString &name) override { m_loggers.remove(name); }
|
||||
|
||||
LogFetchJob *fetchLogEntries(const QStringList &, const QStringList &, const QDateTime &,
|
||||
const QDateTime &, const QVariantMap &, Types::SampleRate,
|
||||
Qt::SortOrder, int, int) override
|
||||
{
|
||||
LogFetchJob *job = new LogFetchJob(this);
|
||||
finishFetchJob(job, LogEntries());
|
||||
return job;
|
||||
}
|
||||
|
||||
bool jobsRunning() const override { return false; }
|
||||
void clear(const QString &) override {}
|
||||
void enable() override {}
|
||||
void disable() override {}
|
||||
|
||||
private:
|
||||
void logEvent(Logger *, const QStringList &, const QVariantMap &) override {}
|
||||
|
||||
QHash<QString, Logger *> m_loggers;
|
||||
};
|
||||
|
||||
class DeviceHardwareManager : public HardwareManager
|
||||
{
|
||||
public:
|
||||
DeviceHardwareManager(const QHostAddress &address, QObject *parent = nullptr)
|
||||
: HardwareManager(parent)
|
||||
, m_pluginTimerManager(new NullPluginTimerManager(this))
|
||||
, m_networkDeviceDiscovery(new DeviceDiscovery(address, this))
|
||||
{}
|
||||
|
||||
Radio433 *radio433() override { return nullptr; }
|
||||
PluginTimerManager *pluginTimerManager() override { return m_pluginTimerManager; }
|
||||
NetworkAccessManager *networkManager() override { return nullptr; }
|
||||
UpnpDiscovery *upnpDiscovery() override { return nullptr; }
|
||||
PlatformZeroConfController *zeroConfController() override { return nullptr; }
|
||||
BluetoothLowEnergyManager *bluetoothLowEnergyManager() override { return nullptr; }
|
||||
MqttProvider *mqttProvider() override { return nullptr; }
|
||||
I2CManager *i2cManager() override { return nullptr; }
|
||||
ZigbeeHardwareResource *zigbeeResource() override { return nullptr; }
|
||||
ZWaveHardwareResource *zwaveResource() override { return nullptr; }
|
||||
ModbusRtuHardwareResource *modbusRtuResource() override { return nullptr; }
|
||||
NetworkDeviceDiscovery *networkDeviceDiscovery() override { return m_networkDeviceDiscovery; }
|
||||
|
||||
private:
|
||||
NullPluginTimerManager *m_pluginTimerManager = nullptr;
|
||||
DeviceDiscovery *m_networkDeviceDiscovery = nullptr;
|
||||
};
|
||||
|
||||
class TestableSunSpecPlugin : public IntegrationPluginSunSpec
|
||||
{
|
||||
public:
|
||||
using IntegrationPlugin::setMetaData;
|
||||
};
|
||||
|
||||
/* -------------------------------------------------------------------------- */
|
||||
|
||||
class SunSpecDeviceTest : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
private slots:
|
||||
void initTestCase();
|
||||
void cleanupTestCase();
|
||||
|
||||
void test_F3_both_connections_reach_the_device();
|
||||
void test_F3_meter_connects_only_after_the_primary_discovery();
|
||||
void test_F3_inverter_and_meter_children_share_one_parent();
|
||||
void test_F3_no_child_disappears_across_repeated_discoveries();
|
||||
|
||||
private:
|
||||
QTemporaryDir m_pluginSearchDir;
|
||||
QHostAddress m_address;
|
||||
int m_port = 502;
|
||||
int m_slaveId = 1;
|
||||
int m_meterSlaveId = 200;
|
||||
|
||||
DeviceHardwareManager *m_hardwareManager = nullptr;
|
||||
ThingManagerImplementation *m_thingManager = nullptr;
|
||||
NullLogEngine *m_logEngine = nullptr;
|
||||
TestableSunSpecPlugin *m_plugin = nullptr;
|
||||
|
||||
bool m_configured = false;
|
||||
/* Vrai des que la connexion compteur signale connected, pour verifier que
|
||||
* cela n'arrive jamais avant la fin de la decouverte primaire. */
|
||||
bool m_meterConnectedBeforePrimaryDiscovery = false;
|
||||
|
||||
bool skipUnlessConfigured();
|
||||
QList<Thing *> childrenOfKind(const ThingClassId &thingClassId) const;
|
||||
};
|
||||
|
||||
static const ThingId DeviceThingId = ThingId("5d3f8a26-41bc-4e79-a05f-9b7c2e6d1483");
|
||||
|
||||
bool SunSpecDeviceTest::skipUnlessConfigured()
|
||||
{
|
||||
if (m_configured)
|
||||
return false;
|
||||
|
||||
/* QSKIP ne peut pas etre appele depuis une fonction auxiliaire, donc le
|
||||
* message est laisse a l'appelant. */
|
||||
return true;
|
||||
}
|
||||
|
||||
QList<Thing *> SunSpecDeviceTest::childrenOfKind(const ThingClassId &thingClassId) const
|
||||
{
|
||||
QList<Thing *> found;
|
||||
foreach (Thing *thing, m_thingManager->configuredThings()) {
|
||||
if (thing->thingClassId() == thingClassId && thing->parentId() == DeviceThingId)
|
||||
found.append(thing);
|
||||
}
|
||||
return found;
|
||||
}
|
||||
|
||||
void SunSpecDeviceTest::initTestCase()
|
||||
{
|
||||
const QByteArray host = qgetenv("SUNSPEC_TEST_HOST");
|
||||
if (host.isEmpty()) {
|
||||
qInfo() << "SUNSPEC_TEST_HOST is not set: every test will be skipped.";
|
||||
return;
|
||||
}
|
||||
|
||||
m_address = QHostAddress(QString::fromUtf8(host));
|
||||
QVERIFY2(!m_address.isNull(), "SUNSPEC_TEST_HOST is not a valid address");
|
||||
|
||||
if (!qgetenv("SUNSPEC_TEST_PORT").isEmpty())
|
||||
m_port = qgetenv("SUNSPEC_TEST_PORT").toInt();
|
||||
if (!qgetenv("SUNSPEC_TEST_SLAVE_ID").isEmpty())
|
||||
m_slaveId = qgetenv("SUNSPEC_TEST_SLAVE_ID").toInt();
|
||||
if (!qgetenv("SUNSPEC_TEST_METER_SLAVE_ID").isEmpty())
|
||||
m_meterSlaveId = qgetenv("SUNSPEC_TEST_METER_SLAVE_ID").toInt();
|
||||
|
||||
QCoreApplication::setOrganizationName("nymea-test-sunspec-device");
|
||||
|
||||
NymeaSettings thingSettings(NymeaSettings::SettingsRoleThings);
|
||||
thingSettings.clear();
|
||||
NymeaSettings pluginSettings(NymeaSettings::SettingsRolePlugins);
|
||||
pluginSettings.clear();
|
||||
|
||||
QVERIFY(m_pluginSearchDir.isValid());
|
||||
qputenv("NYMEA_PLUGINS_PATH", m_pluginSearchDir.path().toUtf8());
|
||||
|
||||
QFile jsonFile(PLUGIN_JSON_PATH);
|
||||
QVERIFY2(jsonFile.open(QIODevice::ReadOnly), qPrintable(QString("Cannot open %1").arg(PLUGIN_JSON_PATH)));
|
||||
QJsonParseError error;
|
||||
const QJsonDocument document = QJsonDocument::fromJson(jsonFile.readAll(), &error);
|
||||
QVERIFY2(error.error == QJsonParseError::NoError, qPrintable(error.errorString()));
|
||||
|
||||
const PluginMetadata metadata(document.object());
|
||||
QVERIFY2(metadata.isValid(), "metadonnee du plugin invalide");
|
||||
|
||||
NymeaSettings settings(NymeaSettings::SettingsRoleThings);
|
||||
settings.beginGroup("ThingConfig");
|
||||
settings.beginGroup(DeviceThingId.toString());
|
||||
settings.setValue("thingName", "Fronius");
|
||||
settings.setValue("pluginid", metadata.pluginId().toString());
|
||||
settings.setValue("thingClassId", froniusConnectionThingClassId.toString());
|
||||
settings.beginGroup("Params");
|
||||
settings.setValue(froniusConnectionThingAddressParamTypeId.toString(), m_address.toString());
|
||||
settings.setValue(froniusConnectionThingPortParamTypeId.toString(), m_port);
|
||||
settings.setValue(froniusConnectionThingSlaveIdParamTypeId.toString(), m_slaveId);
|
||||
settings.setValue(froniusConnectionThingMeterSlaveIdParamTypeId.toString(), m_meterSlaveId);
|
||||
settings.endGroup();
|
||||
settings.endGroup();
|
||||
settings.endGroup();
|
||||
settings.sync();
|
||||
|
||||
m_hardwareManager = new DeviceHardwareManager(m_address, this);
|
||||
m_logEngine = new NullLogEngine(this);
|
||||
m_thingManager = new ThingManagerImplementation(m_hardwareManager, m_logEngine, QLocale(), this);
|
||||
|
||||
m_plugin = new TestableSunSpecPlugin();
|
||||
m_plugin->setMetaData(metadata);
|
||||
m_thingManager->registerStaticPlugin(m_plugin);
|
||||
|
||||
Thing *thing = nullptr;
|
||||
QTRY_VERIFY_WITH_TIMEOUT((thing = m_thingManager->findConfiguredThing(DeviceThingId)) != nullptr, 5000);
|
||||
QTRY_COMPARE_WITH_TIMEOUT(thing->setupStatus(), Thing::ThingSetupStatusComplete, 30000);
|
||||
|
||||
SunSpecConnection *primary = m_plugin->testPrimaryConnection(DeviceThingId);
|
||||
SunSpecConnection *meter = m_plugin->testMeterConnection(DeviceThingId);
|
||||
QVERIFY(primary);
|
||||
QVERIFY2(meter, "aucune connexion compteur creee : verifier SUNSPEC_TEST_METER_SLAVE_ID");
|
||||
|
||||
/* Arme avant toute attente, pour que l'ordre des deux evenements soit
|
||||
* observe et pas reconstitue apres coup. */
|
||||
bool primaryDiscovered = false;
|
||||
connect(primary, &SunSpecConnection::discoveryFinished, this, [&primaryDiscovered](bool success) {
|
||||
if (success)
|
||||
primaryDiscovered = true;
|
||||
});
|
||||
connect(meter, &SunSpecConnection::connectedChanged, this, [this, &primaryDiscovered](bool connected) {
|
||||
if (connected && !primaryDiscovered)
|
||||
m_meterConnectedBeforePrimaryDiscovery = true;
|
||||
});
|
||||
|
||||
m_configured = true;
|
||||
}
|
||||
|
||||
void SunSpecDeviceTest::cleanupTestCase()
|
||||
{
|
||||
delete m_thingManager;
|
||||
m_thingManager = nullptr;
|
||||
}
|
||||
|
||||
void SunSpecDeviceTest::test_F3_both_connections_reach_the_device()
|
||||
{
|
||||
if (skipUnlessConfigured())
|
||||
QSKIP("SUNSPEC_TEST_HOST is not set");
|
||||
|
||||
SunSpecConnection *primary = m_plugin->testPrimaryConnection(DeviceThingId);
|
||||
SunSpecConnection *meter = m_plugin->testMeterConnection(DeviceThingId);
|
||||
QVERIFY(primary && meter);
|
||||
|
||||
QTRY_VERIFY_WITH_TIMEOUT(primary->connected(), 30000);
|
||||
QTRY_VERIFY_WITH_TIMEOUT(meter->connected(), 60000);
|
||||
|
||||
QCOMPARE(static_cast<int>(primary->slaveId()), m_slaveId);
|
||||
QCOMPARE(static_cast<int>(meter->slaveId()), m_meterSlaveId);
|
||||
|
||||
// Les deux ont trouve un registre de base SunSpec, chacune sur son unit ID.
|
||||
QTRY_VERIFY_WITH_TIMEOUT(!primary->models().isEmpty(), 60000);
|
||||
QTRY_VERIFY_WITH_TIMEOUT(!meter->models().isEmpty(), 60000);
|
||||
}
|
||||
|
||||
/* Q4. Deux decouvertes SunSpec simultanees seraient la charge la plus lourde
|
||||
* que ce plugin fasse jamais subir a un onduleur. */
|
||||
void SunSpecDeviceTest::test_F3_meter_connects_only_after_the_primary_discovery()
|
||||
{
|
||||
if (skipUnlessConfigured())
|
||||
QSKIP("SUNSPEC_TEST_HOST is not set");
|
||||
|
||||
QVERIFY2(!m_meterConnectedBeforePrimaryDiscovery,
|
||||
"la connexion compteur est montee avant la fin de la decouverte primaire");
|
||||
}
|
||||
|
||||
/* Un seul appareil a appairer, tous les enfants sous le meme parent. */
|
||||
void SunSpecDeviceTest::test_F3_inverter_and_meter_children_share_one_parent()
|
||||
{
|
||||
if (skipUnlessConfigured())
|
||||
QSKIP("SUNSPEC_TEST_HOST is not set");
|
||||
|
||||
QTRY_VERIFY_WITH_TIMEOUT(!childrenOfKind(sunspecThreePhaseInverterThingClassId).isEmpty()
|
||||
|| !childrenOfKind(sunspecSplitPhaseInverterThingClassId).isEmpty()
|
||||
|| !childrenOfKind(sunspecSinglePhaseInverterThingClassId).isEmpty(), 60000);
|
||||
|
||||
QTRY_VERIFY_WITH_TIMEOUT(!childrenOfKind(sunspecThreePhaseMeterThingClassId).isEmpty()
|
||||
|| !childrenOfKind(sunspecSplitPhaseMeterThingClassId).isEmpty()
|
||||
|| !childrenOfKind(sunspecSinglePhaseMeterThingClassId).isEmpty(), 60000);
|
||||
}
|
||||
|
||||
/* Le defaut le plus certain du multi unit ID : chaque connexion ne voit que la
|
||||
* moitie du tableau, donc un balayage des orphelins fait sur cette moitie ferait
|
||||
* disparaitre les enfants de l'autre. Une redecouverte suffit a le declencher. */
|
||||
void SunSpecDeviceTest::test_F3_no_child_disappears_across_repeated_discoveries()
|
||||
{
|
||||
if (skipUnlessConfigured())
|
||||
QSKIP("SUNSPEC_TEST_HOST is not set");
|
||||
|
||||
const int childrenBefore = m_thingManager->configuredThings().filterByParentId(DeviceThingId).count();
|
||||
QVERIFY2(childrenBefore > 0, "aucun enfant a surveiller");
|
||||
|
||||
QSignalSpy disappeared(m_plugin, &IntegrationPlugin::autoThingDisappeared);
|
||||
|
||||
SunSpecConnection *primary = m_plugin->testPrimaryConnection(DeviceThingId);
|
||||
SunSpecConnection *meter = m_plugin->testMeterConnection(DeviceThingId);
|
||||
|
||||
for (int round = 0; round < 2; round++) {
|
||||
QSignalSpy primaryDone(primary, &SunSpecConnection::discoveryFinished);
|
||||
primary->startDiscovery();
|
||||
QVERIFY2(primaryDone.wait(60000), "la decouverte primaire n'a pas rendu la main");
|
||||
|
||||
QSignalSpy meterDone(meter, &SunSpecConnection::discoveryFinished);
|
||||
meter->startDiscovery();
|
||||
QVERIFY2(meterDone.wait(60000), "la decouverte compteur n'a pas rendu la main");
|
||||
}
|
||||
|
||||
QCOMPARE(disappeared.count(), 0);
|
||||
QCOMPARE(m_thingManager->configuredThings().filterByParentId(DeviceThingId).count(), childrenBefore);
|
||||
}
|
||||
|
||||
QTEST_MAIN(SunSpecDeviceTest)
|
||||
#include "tst_sunspecdevice.moc"
|
||||
@ -9,7 +9,12 @@ TEMPLATE = subdirs
|
||||
# portant deux SunSpecConnection (F3). Binaire distinct pour ne pas
|
||||
# fausser les compteurs d'appels globaux verifies par plugin.
|
||||
|
||||
# integration : le plugin face a un vrai appareil. Construite mais JAMAIS lancee
|
||||
# par "make check" : elle exige un onduleur joignable, passe par
|
||||
# SUNSPEC_TEST_HOST. Voir integration/integration.pro.
|
||||
|
||||
SUBDIRS += \
|
||||
logic \
|
||||
plugin \
|
||||
multiunit
|
||||
multiunit \
|
||||
integration
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user