From ed3d8ef2589686aaf2014a149363a90e6577f0fc Mon Sep 17 00:00:00 2001 From: Boernsman Date: Mon, 22 Feb 2021 14:42:46 +0100 Subject: [PATCH] fixed wallbe device discovery --- wallbe/discover.cpp | 324 ++++++++++++++++++---------- wallbe/discover.h | 55 +++-- wallbe/integrationpluginwallbe.cpp | 79 +++++-- wallbe/integrationpluginwallbe.h | 2 +- wallbe/integrationpluginwallbe.json | 5 +- 5 files changed, 303 insertions(+), 162 deletions(-) diff --git a/wallbe/discover.cpp b/wallbe/discover.cpp index 41d4034..041221b 100644 --- a/wallbe/discover.cpp +++ b/wallbe/discover.cpp @@ -28,138 +28,244 @@ * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ -#include -#include -#include "discover.h" +#include "discovery.h" #include "extern-plugininfo.h" -Discover::Discover(QStringList arguments, QObject *parent) : - QObject(parent), - m_arguments(arguments), - m_discoveryProcess(nullptr), - m_aboutToQuit(false) +#include +#include +#include +#include +#include + +Discovery::Discovery(QObject *parent) : QObject(parent) { + connect(&m_timeoutTimer, &QTimer::timeout, this, &Discovery::onTimeout); } -Discover::~Discover() +void Discovery::discoverHosts(int timeout) { - // Stop running processes - m_aboutToQuit = true; + if (isRunning()) { + qCWarning(dcWallbe()) << "Discovery already running. Cannot start twice."; + return; + } + m_timeoutTimer.start(timeout * 1000); - if (m_discoveryProcess && m_discoveryProcess->state() == QProcess::Running) { - qCDebug(dcWallbe()) << "Kill running discovery process"; - m_discoveryProcess->terminate(); - m_discoveryProcess->waitForFinished(5000); + foreach (const QString &target, getDefaultTargets()) { + QProcess *discoveryProcess = new QProcess(this); + m_discoveryProcesses.append(discoveryProcess); + connect(discoveryProcess, SIGNAL(finished(int, QProcess::ExitStatus)), this, SLOT(discoveryFinished(int,QProcess::ExitStatus))); + + QStringList arguments; + arguments << "-oX" << "-" << "-n" << "-sn"; + arguments << target; + + qCDebug(dcWallbe()) << "Scanning network:" << "nmap" << arguments.join(" "); + discoveryProcess->start(QStringLiteral("nmap"), arguments); } } -QStringList Discover::getDefaultTargets() +void Discovery::abort() +{ + foreach (QProcess *discoveryProcess, m_discoveryProcesses) { + if (discoveryProcess->state() == QProcess::Running) { + qCDebug(dcWallbe()) << "Kill running discovery process"; + discoveryProcess->terminate(); + discoveryProcess->waitForFinished(5000); + } + } + foreach (QProcess *p, m_pendingArpLookups.keys()) { + p->terminate(); + delete p; + } + m_pendingArpLookups.clear(); + m_pendingNameLookups.clear(); + qDeleteAll(m_scanResults); + m_scanResults.clear(); +} + +bool Discovery::isRunning() const +{ + return !m_discoveryProcesses.isEmpty() || !m_pendingArpLookups.isEmpty() || !m_pendingNameLookups.isEmpty(); +} + +void Discovery::discoveryFinished(int exitCode, QProcess::ExitStatus exitStatus) +{ + QProcess *discoveryProcess = static_cast(sender()); + + if (exitCode != 0 || exitStatus != QProcess::NormalExit) { + qCWarning(dcWallbe()) << "Nmap error failed. Is nmap installed correctly?"; + m_discoveryProcesses.removeAll(discoveryProcess); + discoveryProcess->deleteLater(); + discoveryProcess = nullptr; + finishDiscovery(); + return; + } + + QByteArray data = discoveryProcess->readAll(); + m_discoveryProcesses.removeAll(discoveryProcess); + discoveryProcess->deleteLater(); + discoveryProcess = nullptr; + + QXmlStreamReader reader(data); + + int foundHosts = 0; + qCDebug(dcWallbe()) << "nmap finished network discovery:"; + while (!reader.atEnd() && !reader.hasError()) { + QXmlStreamReader::TokenType token = reader.readNext(); + if(token == QXmlStreamReader::StartDocument) + continue; + + if(token == QXmlStreamReader::StartElement && reader.name() == "host") { + bool isUp = false; + QString address; + QString macAddress; + QString vendor; + while (!reader.atEnd() && !reader.hasError() && !(token == QXmlStreamReader::EndElement && reader.name() == "host")) { + token = reader.readNext(); + + if (reader.name() == "address") { + QString addr = reader.attributes().value("addr").toString(); + QString type = reader.attributes().value("addrtype").toString(); + if (type == "ipv4" && !addr.isEmpty()) { + address = addr; + } else if (type == "mac") { + macAddress = addr; + vendor = reader.attributes().value("vendor").toString(); + } + } + + if (reader.name() == "status") { + QString state = reader.attributes().value("state").toString(); + if (!state.isEmpty()) + isUp = state == "up"; + } + } + + if (isUp) { + foundHosts++; + qCDebug(dcWallbe()) << " - host:" << address; + + Host *host = new Host(); + host->setAddress(address); + + if (!macAddress.isEmpty()) { + host->setMacAddress(macAddress); + } else { + QProcess *arpLookup = new QProcess(this); + m_pendingArpLookups.insert(arpLookup, host); + connect(arpLookup, SIGNAL(finished(int, QProcess::ExitStatus)), this, SLOT(arpLookupDone(int,QProcess::ExitStatus))); + arpLookup->start("arp", {"-vn"}); + } + + host->setHostName(vendor); + QHostInfo::lookupHost(address, this, SLOT(hostLookupDone(QHostInfo))); + m_pendingNameLookups.insert(address, host); + + m_scanResults.append(host); + } + } + } + + if (foundHosts == 0 && m_discoveryProcesses.isEmpty()) { + qCDebug(dcWallbe()) << "Network scan successful but no hosts found in this network"; + finishDiscovery(); + } +} + +void Discovery::hostLookupDone(const QHostInfo &info) +{ + Host *host = m_pendingNameLookups.take(info.addresses().first().toString()); + if (!host) { + // Probably aborted... + return; + } + if (info.error() != QHostInfo::NoError) { + qWarning(dcWallbe()) << "Host lookup failed:" << info.errorString(); + } + if (host->hostName().isEmpty() || info.hostName() != host->address()) { + host->setHostName(info.hostName()); + } + + finishDiscovery(); +} + +void Discovery::arpLookupDone(int exitCode, QProcess::ExitStatus exitStatus) +{ + QProcess *p = static_cast(sender()); + p->deleteLater(); + + Host *host = m_pendingArpLookups.take(p); + + if (exitCode != 0 || exitStatus != QProcess::NormalExit) { + qCWarning(dcWallbe()) << "ARP lookup process failed for host" << host->address(); + finishDiscovery(); + return; + } + + QString data = QString::fromLatin1(p->readAll()); + foreach (QString line, data.split('\n')) { + line.replace(QRegExp("[ ]{1,}"), " "); + QStringList parts = line.split(" "); + if (parts.count() >= 3 && parts.first() == host->address() && parts.at(1) == "ether") { + host->setMacAddress(parts.at(2)); + break; + } + } + finishDiscovery(); +} + +void Discovery::onTimeout() +{ + qWarning(dcWallbe()) << "Timeout hit. Stopping discovery"; + while (!m_discoveryProcesses.isEmpty()) { + QProcess *discoveryProcess = m_discoveryProcesses.takeFirst(); + disconnect(this, SLOT(discoveryFinished(int,QProcess::ExitStatus))); + discoveryProcess->terminate(); + delete discoveryProcess; + } + foreach (QProcess *p, m_pendingArpLookups.keys()) { + p->terminate(); + m_scanResults.removeAll(m_pendingArpLookups.value(p)); + delete p; + } + m_pendingArpLookups.clear(); + m_pendingNameLookups.clear(); + finishDiscovery(); +} + +QStringList Discovery::getDefaultTargets() { QStringList targets; foreach (const QHostAddress &interface, QNetworkInterface::allAddresses()) { - if (!interface.isLoopback() && interface.scopeId().isEmpty()) { + if (!interface.isLoopback() && interface.scopeId().isEmpty() && interface.protocol() == QAbstractSocket::IPv4Protocol) { QPair pair = QHostAddress::parseSubnet(interface.toString() + "/24"); - targets << QString("%1/%2").arg(pair.first.toString()).arg(pair.second); + QString newTarget = QString("%1/%2").arg(pair.first.toString()).arg(pair.second); + if (!targets.contains(newTarget)) { + targets.append(newTarget); + } } } return targets; } -QList Discover::parseProcessOutput(const QByteArray &processData) +void Discovery::finishDiscovery() { - m_reader.clear(); - m_reader.addData(processData); + if (m_discoveryProcesses.count() > 0 || m_pendingNameLookups.count() > 0 || m_pendingArpLookups.count() > 0) { + // Still busy... + return; + } QList hosts; - - while (!m_reader.atEnd() && !m_reader.hasError()) { - - QXmlStreamReader::TokenType token = m_reader.readNext(); - if(token == QXmlStreamReader::StartDocument) - continue; - - if(token == QXmlStreamReader::StartElement && m_reader.name() == "host") { - Host host = parseHost(); - if (host.isValid()) { - hosts.append(host); - } + foreach (Host *host, m_scanResults) { + if (!host->macAddress().isEmpty()) { + hosts.append(*host); } } - return hosts; + qDeleteAll(m_scanResults); + m_scanResults.clear(); + + qCDebug(dcWallbe()) << "Found" << hosts.count() << "network devices"; + m_timeoutTimer.stop(); + emit finished(hosts); } -Host Discover::parseHost() -{ - if (!m_reader.isStartElement() || m_reader.name() != "host") - return Host(); - - QString address; QString hostName; QString status; QString mac; - while(!(m_reader.tokenType() == QXmlStreamReader::EndElement && m_reader.name() == "host")){ - - m_reader.readNext(); - - if (m_reader.isStartElement() && m_reader.name() == "hostname") { - QString name = m_reader.attributes().value("name").toString(); - if (!name.isEmpty()) - hostName = name; - - m_reader.readNext(); - } - - if (m_reader.name() == "address") { - QString addr = m_reader.attributes().value("addr").toString(); - if (!addr.isEmpty()) - address = addr; - } - - if (m_reader.name() == "state") { - QString state = m_reader.attributes().value("state").toString(); - if (!state.isEmpty()) - status = state; - } - - if (m_reader.name() == "mac") { //TODO CHeck Keyword - QString macAddress = m_reader.attributes().value("state").toString(); - if (!macAddress.isEmpty()) - mac = macAddress; - } - } - return Host(hostName, address, mac, (status == "open" ? true : false)); -} - -void Discover::processFinished(int exitCode, QProcess::ExitStatus exitStatus) -{ - QProcess *process = static_cast(sender()); - - // If the process was killed because nymead is shutting down...we dont't care any more about the result - if (m_aboutToQuit) - return; - - // Discovery - if (process == m_discoveryProcess) { - - qCDebug(dcWallbe()) << "Discovery process finished"; - - process->deleteLater(); - m_discoveryProcess = nullptr; - - if (exitCode != 0 || exitStatus != QProcess::NormalExit) { - qCWarning(dcWallbe()) << "Network scan error:" << process->readAllStandardError(); - return; - } - - QByteArray outputData = process->readAllStandardOutput(); - emit devicesDiscovered(parseProcessOutput(outputData)); - - } -} - -bool Discover::getProcessStatus() -{ - return m_discoveryProcess; -} - -void Discover::onTimeout() -{ - -} diff --git a/wallbe/discover.h b/wallbe/discover.h index 6e07191..02284c3 100644 --- a/wallbe/discover.h +++ b/wallbe/discover.h @@ -28,49 +28,48 @@ * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ -#ifndef DISCOVER_H -#define DISCOVER_H +#ifndef DISCOVERY_H +#define DISCOVERY_H #include #include -#include +#include #include -#include #include "host.h" -class Discover : public QObject +class Discovery : public QObject { Q_OBJECT public: + explicit Discovery(QObject *parent = nullptr); - explicit Discover(QStringList arguments, QObject *parent = nullptr); - ~Discover(); - bool getProcessStatus(); - -private: - QTimer *m_timer; - QStringList m_arguments; - - QProcess * m_discoveryProcess; - QProcess * m_scanProcess; - - QXmlStreamReader m_reader; - - bool m_aboutToQuit; - - QStringList getDefaultTargets(); - void processFinished(int exitCode, QProcess::ExitStatus exitStatus); - - // Process parsing - QList parseProcessOutput(const QByteArray &processData); - Host parseHost(); + void discoverHosts(int timeout); + void abort(); + bool isRunning() const; signals: - void devicesDiscovered(QList); + void finished(const QList &hosts); + +private: + QStringList getDefaultTargets(); + + void finishDiscovery(); private slots: + void discoveryFinished(int exitCode, QProcess::ExitStatus exitStatus); + void hostLookupDone(const QHostInfo &info); + void arpLookupDone(int exitCode, QProcess::ExitStatus exitStatus); void onTimeout(); + +private: + QList m_discoveryProcesses; + QTimer m_timeoutTimer; + + QHash m_pendingArpLookups; + QHash m_pendingNameLookups; + QList m_scanResults; }; -#endif // DISCOVER_H +#endif // DISCOVERY_H + diff --git a/wallbe/integrationpluginwallbe.cpp b/wallbe/integrationpluginwallbe.cpp index 27bb10b..23898cf 100644 --- a/wallbe/integrationpluginwallbe.cpp +++ b/wallbe/integrationpluginwallbe.cpp @@ -43,31 +43,29 @@ IntegrationPluginWallbe::IntegrationPluginWallbe() { } +void IntegrationPluginWallbe::init() { -void IntegrationPluginWallbe::setupThing(ThingSetupInfo *info) -{ - Thing *thing = info->thing(); - qCDebug(dcWallbe) << "Setting up a new device:" << thing->params(); + m_discovery = new Discover(QStringList("-xO" "-p 502")); + connect(m_discovery, &Discovery::finished, this, [this](const QList &hosts) { - QHostAddress address(thing->paramValue(wallbeEcoThingIpParamTypeId).toString()); + Q_FOREACH(Thing *existingThing, myThings()) { + if (existingThing->paramValue(wallbeEcoThingMacAddressParamTypeId).toString().isEmpty()) { + //This device got probably manually setup, to enable auto rediscovery the MAC address needs to setup + if (existingThing->paramValue(wallbeEcoThingIpParamTypeId).toString() == host.address()) { + qCDebug(dcWallbe()) << "Wallbe Wallbox MAC Address has been discovered" << existingThing->name() << host.macAddress(); + existingThing->setParamValue(wallbeEcoThingMacParamTypeId, host.macAddress()); - if (address.isNull()){ - qCWarning(dcWallbe) << "IP address is not valid"; - info->finish(Thing::ThingErrorSetupFailed, tr("Invalid IP address")); - return; - } - ModbusTCPMaster *modbusTcpMaster = new ModbusTCPMaster(address, 502, this); - connect(modbusTcpMaster, &ModbusTCPMaster::connectionStateChanged, this, &IntegrationPluginWallbe::onConnectionStateChanged); - connect(modbusTcpMaster, &ModbusTCPMaster::receivedHoldingRegister, this, &IntegrationPluginWallbe::onReceivedHoldingRegister); - connect(modbusTcpMaster, &ModbusTCPMaster::writeRequestExecuted, this, &IntegrationPluginWallbe::onWriteRequestExecuted); - connect(modbusTcpMaster, &ModbusTCPMaster::writeRequestError, this, &IntegrationPluginWallbe::onWriteRequestError); + } + } else if (existingThing->paramValue(wallbeEcoThingMacParamTypeId).toString() == host.macAddress()) { + if (existingThing->paramValue(wallbeEcoThingIpParamTypeId).toString() != host.address()) { + qCDebug(dcWallbe()) << "Wallbe Wallbox IP Address has changed, from" << existingThing->paramValue(wallbeEcoThingIpParamTypeId).toString() << "to" << host.address(); + existingThing->setParamValue(wallbeEcoThingIpParamTypeId, host.address()); - m_connections.insert(thing, modbusTcpMaster); - connect(modbusTcpMaster, &ModbusTCPMaster::connectionStateChanged, info, [this, info, modbusTcpMaster](bool connected) { - if(connected) { - info->finish(Thing::ThingErrorNoError); - } else { - info->finish(Thing::ThingErrorHardwareNotAvailable); + } else { + qCDebug(dcWallbe()) << "Wallbe Wallbox" << existingThing->name() << "IP address has not changed" << host.address(); + } + break; + } } }); } @@ -76,8 +74,9 @@ void IntegrationPluginWallbe::setupThing(ThingSetupInfo *info) void IntegrationPluginWallbe::discoverThings(ThingDiscoveryInfo *info) { if (info->thingClassId() == wallbeEcoThingClassId){ + qCDebug(dcWallbe) << "Start Wallbe eco discovery"; - Discover *discover = new Discover(QStringList("-xO" "-p 502")); + m_discovery->discoverHosts(25); connect(discover, &Discover::devicesDiscovered, this, [info, this](QList hosts){ foreach (Host host, hosts) { ThingDescriptor descriptor; @@ -99,6 +98,42 @@ void IntegrationPluginWallbe::discoverThings(ThingDiscoveryInfo *info) } } + +void IntegrationPluginWallbe::setupThing(ThingSetupInfo *info) +{ + Thing *thing = info->thing(); + qCDebug(dcWallbe) << "Setting up a new device:" << thing->params(); + + if (info->thingClassId() == wallbeEcoThingClassId) { + QHostAddress address(thing->paramValue(wallbeEcoThingIpParamTypeId).toString()); + + if (m_connections.contains(thing)) { + qCDebug(dcWallbe()) << "Setup after reconfiguration, cleaning up ..."; + m_connnections.take(thing).deleteLater();) + } + if (address.isNull()){ + qCWarning(dcWallbe) << "IP address is not valid"; + info->finish(Thing::ThingErrorSetupFailed, tr("Invalid IP address")); + return; + } + ModbusTCPMaster *modbusTcpMaster = new ModbusTCPMaster(address, 502, this); + connect(modbusTcpMaster, &ModbusTCPMaster::connectionStateChanged, this, &IntegrationPluginWallbe::onConnectionStateChanged); + connect(modbusTcpMaster, &ModbusTCPMaster::receivedHoldingRegister, this, &IntegrationPluginWallbe::onReceivedHoldingRegister); + connect(modbusTcpMaster, &ModbusTCPMaster::writeRequestExecuted, this, &IntegrationPluginWallbe::onWriteRequestExecuted); + connect(modbusTcpMaster, &ModbusTCPMaster::writeRequestError, this, &IntegrationPluginWallbe::onWriteRequestError); + + m_connections.insert(thing, modbusTcpMaster); + connect(modbusTcpMaster, &ModbusTCPMaster::connectionStateChanged, info, [this, info, modbusTcpMaster](bool connected) { + if(connected) { + info->finish(Thing::ThingErrorNoError); + } else { + info->finish(Thing::ThingErrorHardwareNotAvailable); + } + }); + } +} + + void IntegrationPluginWallbe::postSetupThing(Thing *thing) { if (!m_pluginTimer) { diff --git a/wallbe/integrationpluginwallbe.h b/wallbe/integrationpluginwallbe.h index b6edb84..56a4fe0 100644 --- a/wallbe/integrationpluginwallbe.h +++ b/wallbe/integrationpluginwallbe.h @@ -58,7 +58,7 @@ public: }; explicit IntegrationPluginWallbe(); - + void init() override; void discoverThings(ThingDiscoveryInfo *info) override; void setupThing(ThingSetupInfo *info) override; void postSetupThing(Thing *thing) override; diff --git a/wallbe/integrationpluginwallbe.json b/wallbe/integrationpluginwallbe.json index e7c5bd0..73a165e 100644 --- a/wallbe/integrationpluginwallbe.json +++ b/wallbe/integrationpluginwallbe.json @@ -12,7 +12,7 @@ "id": "e66c84f6-b398-47e9-8aeb-33840e7b4492", "displayName": "Wallbe eco 2.0", "name": "wallbeEco", - "createMethods": ["discovery"], + "createMethods": ["discovery", "user"], "interfaces": ["evcharger", "connectable"], "paramTypes": [ { @@ -27,7 +27,8 @@ "displayName": "MAC address", "name": "mac", "type": "QString", - "defaultValue": "" + "defaultValue": "", + "readOnly": true } ], "stateTypes":[