feat(v2c): add HTTP LAN discovery (V2cTcpDiscovery)
Architecture choice: Modbus TCP has no network discovery mechanism, so
discovery piggybacks on nymea's NetworkDeviceDiscovery (ARP/mDNS scan)
and then probes each host via GET /RealTimeData (V2C Trydan HTTP REST API).
The presence of a "Paused" field in the JSON response is used as a Trydan
fingerprint — it matches the charger model and is absent on generic devices.
cf. evcc charger/trydan.go realTimeData struct.
Probes run concurrently (one QNetworkReply per host) but the sequence
terminates via two independent paths that could both fire:
1. The NetworkDeviceDiscoveryReply::finished callback arms a 3-second
QTimer::singleShot to give outstanding probes time to complete.
2. Each probe completion path checks (m_scanFinished && pendingProbes==0)
and calls finishDiscovery() early if all probes are already done.
A m_finished bool guard prevents double-emission of discoveryFinished()
if both paths fire in quick succession (e.g. all probes complete exactly
as the timer fires).
Manual-entry (createMethod "user") skips discovery entirely; setupThing()
is called directly with the user-supplied IP.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
14f07cc638
commit
947da7e1fc
110
v2c/v2ctcpdiscovery.cpp
Normal file
110
v2c/v2ctcpdiscovery.cpp
Normal file
@ -0,0 +1,110 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#include "v2ctcpdiscovery.h"
|
||||
#include "extern-plugininfo.h"
|
||||
|
||||
#include <QNetworkReply>
|
||||
#include <QNetworkRequest>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QTimer>
|
||||
#include <QTime>
|
||||
#include <QUrl>
|
||||
|
||||
V2cTcpDiscovery::V2cTcpDiscovery(NetworkDeviceDiscovery *networkDeviceDiscovery,
|
||||
QObject *parent)
|
||||
: QObject(parent),
|
||||
m_networkDeviceDiscovery(networkDeviceDiscovery)
|
||||
{}
|
||||
|
||||
void V2cTcpDiscovery::startDiscovery()
|
||||
{
|
||||
qCInfo(dcV2c()) << "Discovery: searching for V2C Trydan chargers on the network...";
|
||||
m_startTime = QDateTime::currentDateTime();
|
||||
m_pendingProbes = 0;
|
||||
m_scanFinished = false;
|
||||
m_results.clear();
|
||||
m_networkDeviceInfos.clear();
|
||||
|
||||
NetworkDeviceDiscoveryReply *reply = m_networkDeviceDiscovery->discover();
|
||||
|
||||
connect(reply, &NetworkDeviceDiscoveryReply::hostAddressDiscovered,
|
||||
this, &V2cTcpDiscovery::probeAddress);
|
||||
|
||||
connect(reply, &NetworkDeviceDiscoveryReply::finished,
|
||||
reply, &NetworkDeviceDiscoveryReply::deleteLater);
|
||||
|
||||
connect(reply, &NetworkDeviceDiscoveryReply::finished, this, [this, reply]() {
|
||||
m_networkDeviceInfos = reply->networkDeviceInfos();
|
||||
m_scanFinished = true;
|
||||
|
||||
// Give outstanding HTTP probes 3 extra seconds to complete.
|
||||
QTimer::singleShot(3000, this, &V2cTcpDiscovery::finishDiscovery);
|
||||
});
|
||||
}
|
||||
|
||||
QList<V2cTcpDiscovery::Result> V2cTcpDiscovery::results() const
|
||||
{
|
||||
return m_results;
|
||||
}
|
||||
|
||||
void V2cTcpDiscovery::probeAddress(const QHostAddress &address)
|
||||
{
|
||||
// The V2C Trydan HTTP API: GET /RealTimeData returns JSON with a "Paused"
|
||||
// field (and ChargeState, ChargePower, etc.). The presence of "Paused"
|
||||
// is a reliable Trydan fingerprint; it does not appear on generic devices.
|
||||
// cf. evcc charger/trydan.go realTimeData struct and AGENTS.md § HTTP.
|
||||
QUrl url;
|
||||
url.setScheme(QStringLiteral("http"));
|
||||
url.setHost(address.toString());
|
||||
url.setPath(QStringLiteral("/RealTimeData"));
|
||||
|
||||
QNetworkRequest request(url);
|
||||
request.setTransferTimeout(3000);
|
||||
|
||||
m_pendingProbes++;
|
||||
QNetworkReply *reply = m_nam.get(request);
|
||||
|
||||
connect(reply, &QNetworkReply::finished, this, [this, reply, address]() {
|
||||
reply->deleteLater();
|
||||
m_pendingProbes--;
|
||||
|
||||
if (reply->error() == QNetworkReply::NoError) {
|
||||
const QByteArray data = reply->readAll();
|
||||
const QJsonObject json = QJsonDocument::fromJson(data).object();
|
||||
|
||||
// "Paused" is the HTTP-API name for PauseState — note it differs from
|
||||
// the Modbus register name (cf. AGENTS.md § HTTP).
|
||||
if (json.contains(QStringLiteral("Paused"))) {
|
||||
Result result;
|
||||
result.ipAddress = address.toString();
|
||||
result.networkDeviceInfo = m_networkDeviceInfos.get(address);
|
||||
if (result.networkDeviceInfo.address().isNull()) {
|
||||
NetworkDeviceInfo info;
|
||||
info.setAddress(address);
|
||||
result.networkDeviceInfo = info;
|
||||
}
|
||||
qCDebug(dcV2c()) << "Discovery: found V2C Trydan at" << address.toString();
|
||||
m_results.append(result);
|
||||
}
|
||||
}
|
||||
|
||||
// Finish early if the scan is already done and this was the last probe.
|
||||
if (m_scanFinished && m_pendingProbes == 0)
|
||||
finishDiscovery();
|
||||
});
|
||||
}
|
||||
|
||||
void V2cTcpDiscovery::finishDiscovery()
|
||||
{
|
||||
// Guard against double-emission (can be called by the timer and the last probe).
|
||||
if (m_finished)
|
||||
return;
|
||||
m_finished = true;
|
||||
|
||||
const qint64 ms = QDateTime::currentMSecsSinceEpoch() - m_startTime.toMSecsSinceEpoch();
|
||||
qCInfo(dcV2c()) << "Discovery: finished in"
|
||||
<< QTime::fromMSecsSinceStartOfDay(static_cast<int>(ms)).toString("mm:ss.zzz")
|
||||
<< "with" << m_results.count() << "V2C Trydan(s) found.";
|
||||
emit discoveryFinished();
|
||||
}
|
||||
61
v2c/v2ctcpdiscovery.h
Normal file
61
v2c/v2ctcpdiscovery.h
Normal file
@ -0,0 +1,61 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#ifndef V2CTCPDISCOVERY_H
|
||||
#define V2CTCPDISCOVERY_H
|
||||
|
||||
#include <QObject>
|
||||
#include <QDateTime>
|
||||
#include <QNetworkAccessManager>
|
||||
#include <QHash>
|
||||
|
||||
#include <network/networkdevicediscovery.h>
|
||||
|
||||
/*!
|
||||
* \brief Network discovery for V2C Trydan chargers (Modbus TCP).
|
||||
*
|
||||
* Uses nymea's NetworkDeviceDiscovery to enumerate LAN hosts, then probes
|
||||
* each one with \c GET /RealTimeData (the V2C Trydan HTTP REST API) to
|
||||
* confirm it is a Trydan. The probe is HTTP — not Modbus — because the
|
||||
* Trydan has no network-accessible Modbus discovery mechanism.
|
||||
* cf. AGENTS.md § HTTP and evcc charger/trydan.go.
|
||||
*
|
||||
* Manual-entry (createMethod "user") bypasses discovery entirely;
|
||||
* the plugin calls setupThing() directly with the user-supplied IP.
|
||||
*/
|
||||
class V2cTcpDiscovery : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
struct Result {
|
||||
NetworkDeviceInfo networkDeviceInfo;
|
||||
QString ipAddress;
|
||||
};
|
||||
|
||||
explicit V2cTcpDiscovery(NetworkDeviceDiscovery *networkDeviceDiscovery,
|
||||
QObject *parent = nullptr);
|
||||
|
||||
void startDiscovery();
|
||||
|
||||
/*! \brief Results available after discoveryFinished() is emitted. */
|
||||
QList<Result> results() const;
|
||||
|
||||
signals:
|
||||
void discoveryFinished();
|
||||
|
||||
private:
|
||||
void probeAddress(const QHostAddress &address);
|
||||
void finishDiscovery();
|
||||
|
||||
NetworkDeviceDiscovery *m_networkDeviceDiscovery = nullptr;
|
||||
QNetworkAccessManager m_nam;
|
||||
|
||||
QDateTime m_startTime;
|
||||
NetworkDeviceInfos m_networkDeviceInfos;
|
||||
int m_pendingProbes = 0;
|
||||
QList<Result> m_results;
|
||||
bool m_scanFinished = false;
|
||||
bool m_finished = false;
|
||||
};
|
||||
|
||||
#endif // V2CTCPDISCOVERY_H
|
||||
Loading…
x
Reference in New Issue
Block a user