// SPDX-License-Identifier: GPL-3.0-or-later #include "v2ctcpdiscovery.h" #include "extern-plugininfo.h" #include #include #include #include #include #include #include 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::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(ms)).toString("mm:ss.zzz") << "with" << m_results.count() << "V2C Trydan(s) found."; emit discoveryFinished(); }