diff --git a/fronius/fronius.pro b/fronius/fronius.pro index e730ebf0..84e9ebd3 100644 --- a/fronius/fronius.pro +++ b/fronius/fronius.pro @@ -3,11 +3,13 @@ include(../plugins.pri) QT += network SOURCES += \ + froniusdiscovery.cpp \ froniusnetworkreply.cpp \ froniussolarconnection.cpp \ integrationpluginfronius.cpp \ HEADERS += \ + froniusdiscovery.h \ froniusnetworkreply.h \ froniussolarconnection.h \ integrationpluginfronius.h \ diff --git a/fronius/froniusdiscovery.cpp b/fronius/froniusdiscovery.cpp new file mode 100644 index 00000000..997ce59b --- /dev/null +++ b/fronius/froniusdiscovery.cpp @@ -0,0 +1,137 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* Copyright 2013 - 2023, nymea GmbH +* Contact: contact@nymea.io +* +* This file is part of nymea. +* This project including source code and documentation is protected by +* copyright law, and remains the property of nymea GmbH. All rights, including +* reproduction, publication, editing and translation, are reserved. The use of +* this project is subject to the terms of a license agreement to be concluded +* with nymea GmbH in accordance with the terms of use of nymea GmbH, available +* under https://nymea.io/license +* +* GNU Lesser General Public License Usage +* Alternatively, this project may be redistributed and/or modified under the +* terms of the GNU Lesser General Public License as published by the Free +* Software Foundation; version 3. This project 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 +* Lesser General Public License for more details. +* +* You should have received a copy of the GNU Lesser General Public License +* along with this project. If not, see . +* +* For any further details and any questions please contact us under +* contact@nymea.io or see our FAQ/Licensing Information on +* https://nymea.io/license/faq +* +* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +#include "froniusdiscovery.h" + +#include "extern-plugininfo.h" + +#include +#include + +FroniusDiscovery::FroniusDiscovery(NetworkAccessManager *networkManager, NetworkDeviceDiscovery *networkDeviceDiscovery, QObject *parent): + QObject(parent), + m_networkManager(networkManager), + m_networkDeviceDiscovery{networkDeviceDiscovery} +{ + m_gracePeriodTimer.setSingleShot(true); + m_gracePeriodTimer.setInterval(3000); + connect(&m_gracePeriodTimer, &QTimer::timeout, this, [this](){ + qCDebug(dcFronius()) << "Discovery: Grace period timer triggered."; + finishDiscovery(); + }); + +} + +void FroniusDiscovery::startDiscovery() +{ + qCDebug(dcFronius()) << "Discovery: Searching for Fronius solar devices in the network..."; + m_startDateTime = QDateTime::currentDateTime(); + + NetworkDeviceDiscoveryReply *discoveryReply = m_networkDeviceDiscovery->discover(); + connect(discoveryReply, &NetworkDeviceDiscoveryReply::networkDeviceInfoAdded, this, &FroniusDiscovery::checkNetworkDevice); + connect(discoveryReply, &NetworkDeviceDiscoveryReply::finished, this, [=](){ + qCDebug(dcFronius()) << "Discovery: Network discovery finished. Found" << discoveryReply->networkDeviceInfos().count() << "network devices"; + m_gracePeriodTimer.start(); + discoveryReply->deleteLater(); + }); +} + +QList FroniusDiscovery::discoveryResults() const +{ + return m_discoveryResults; +} + +void FroniusDiscovery::checkNetworkDevice(const NetworkDeviceInfo &networkDeviceInfo) +{ + qCDebug(dcFronius()) << "Discovery: Checking network device:" << networkDeviceInfo; + + FroniusSolarConnection *connection = new FroniusSolarConnection(m_networkManager, networkDeviceInfo.address(), this); + m_connections.append(connection); + + FroniusNetworkReply *reply = connection->getVersion(); + connect(reply, &FroniusNetworkReply::finished, this, [=] { + QByteArray data = reply->networkReply()->readAll(); + if (reply->networkReply()->error() != QNetworkReply::NoError) { + if (reply->networkReply()->error() == QNetworkReply::ContentNotFoundError) { + qCInfo(dcFronius()) << "Discovery: The device on" << networkDeviceInfo.address().toString() << "does not reply to our requests. Please verify that the Fronius Solar API is enabled on the device."; + } else { + qCDebug(dcFronius()) << "Discovery: Reply finished with error on" << networkDeviceInfo.address().toString() << reply->networkReply()->errorString(); + } + cleanupConnection(connection); + return; + } + + QJsonParseError error; + QJsonDocument jsonDoc = QJsonDocument::fromJson(data, &error); + if (error.error != QJsonParseError::NoError) { + qCDebug(dcFronius()) << "Discovery: Failed to parse JSON data from" << networkDeviceInfo.address().toString() << ":" << error.errorString() << data; + cleanupConnection(connection); + return; + } + + QVariantMap versionResponseMap = jsonDoc.toVariant().toMap(); + if (!versionResponseMap.contains("CompatibilityRange")) { + qCDebug(dcFronius()) << "Discovery: Unexpected JSON reply from" << networkDeviceInfo.address().toString() << "Probably not a Fronius device."; + cleanupConnection(connection); + return; + } + qCDebug(dcFronius()) << "Discovery: Compatibility version" << versionResponseMap.value("CompatibilityRange").toString(); + + // Knwon version with broken JSON API. Still allowing to discover so the user will get a proper error message during setup + if (versionResponseMap.value("CompatibilityRange").toString() == "1.6-2") { + qCWarning(dcFronius()) << "Discovery: The Fronius data logger has a version which is known to have a broken JSON API firmware."; + } + + m_discoveryResults.append(networkDeviceInfo); + cleanupConnection(connection); + }); +} + +void FroniusDiscovery::cleanupConnection(FroniusSolarConnection *connection) +{ + m_connections.removeAll(connection); + connection->deleteLater(); +} + +void FroniusDiscovery::finishDiscovery() +{ + qint64 durationMilliSeconds = QDateTime::currentMSecsSinceEpoch() - m_startDateTime.toMSecsSinceEpoch(); + + foreach (FroniusSolarConnection *connection, m_connections) { + cleanupConnection(connection); + } + + qCDebug(dcFronius()) << "Discovery: Finished the discovery process. Found" << m_discoveryResults.count() + << "Fronius devices in" << QTime::fromMSecsSinceStartOfDay(durationMilliSeconds).toString("mm:ss.zzz"); + m_gracePeriodTimer.stop(); + + emit discoveryFinished(); + +} diff --git a/fronius/froniusdiscovery.h b/fronius/froniusdiscovery.h new file mode 100644 index 00000000..6b7b3249 --- /dev/null +++ b/fronius/froniusdiscovery.h @@ -0,0 +1,70 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* Copyright 2013 - 2023, nymea GmbH +* Contact: contact@nymea.io +* +* This file is part of nymea. +* This project including source code and documentation is protected by +* copyright law, and remains the property of nymea GmbH. All rights, including +* reproduction, publication, editing and translation, are reserved. The use of +* this project is subject to the terms of a license agreement to be concluded +* with nymea GmbH in accordance with the terms of use of nymea GmbH, available +* under https://nymea.io/license +* +* GNU Lesser General Public License Usage +* Alternatively, this project may be redistributed and/or modified under the +* terms of the GNU Lesser General Public License as published by the Free +* Software Foundation; version 3. This project 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 +* Lesser General Public License for more details. +* +* You should have received a copy of the GNU Lesser General Public License +* along with this project. If not, see . +* +* For any further details and any questions please contact us under +* contact@nymea.io or see our FAQ/Licensing Information on +* https://nymea.io/license/faq +* +* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +#ifndef FRONIUSDISCOVERY_H +#define FRONIUSDISCOVERY_H + +#include +#include + +#include +#include "froniussolarconnection.h" + +class FroniusDiscovery : public QObject +{ + Q_OBJECT +public: + explicit FroniusDiscovery(NetworkAccessManager *networkManager, NetworkDeviceDiscovery *networkDeviceDiscovery, QObject *parent = nullptr); + + void startDiscovery(); + + QList discoveryResults() const; + +signals: + void discoveryFinished(); + +private: + NetworkAccessManager *m_networkManager = nullptr; + NetworkDeviceDiscovery *m_networkDeviceDiscovery = nullptr; + + QTimer m_gracePeriodTimer; + QDateTime m_startDateTime; + + QList m_connections; + + QList m_discoveryResults; + + void checkNetworkDevice(const NetworkDeviceInfo &networkDeviceInfo); + void cleanupConnection(FroniusSolarConnection *connection); + + void finishDiscovery(); +}; + +#endif // FRONIUSDISCOVERY_H diff --git a/fronius/froniussolarconnection.cpp b/fronius/froniussolarconnection.cpp index d34bfe4d..1566eac9 100644 --- a/fronius/froniussolarconnection.cpp +++ b/fronius/froniussolarconnection.cpp @@ -46,6 +46,30 @@ QHostAddress FroniusSolarConnection::address() const return m_address; } +void FroniusSolarConnection::setAddress(const QHostAddress &address) +{ + if (m_address == address) + return; + + m_address = address; + + // The address has changed, let's clean up any queue and refresh + + // Note: the destructor will take care about the cleanup of any pending replies + qDeleteAll(m_requestQueue); + m_requestQueue.clear(); + + if (m_currentReply) { + m_currentReply->deleteLater(); + m_currentReply = nullptr; + } + + if (m_address.isNull()) { + m_available = false; + emit availableChanged(m_available); + } +} + bool FroniusSolarConnection::available() const { return m_available; diff --git a/fronius/froniussolarconnection.h b/fronius/froniussolarconnection.h index e940a9be..df5bec99 100644 --- a/fronius/froniussolarconnection.h +++ b/fronius/froniussolarconnection.h @@ -32,7 +32,6 @@ #define FRONIUSSOLARCONNECTION_H #include - #include #include @@ -47,6 +46,7 @@ public: explicit FroniusSolarConnection(NetworkAccessManager *networkManager, const QHostAddress &address, QObject *parent = nullptr); QHostAddress address() const; + void setAddress(const QHostAddress &address); bool available() const; diff --git a/fronius/integrationpluginfronius.cpp b/fronius/integrationpluginfronius.cpp index f46949a9..6099c8d4 100644 --- a/fronius/integrationpluginfronius.cpp +++ b/fronius/integrationpluginfronius.cpp @@ -28,11 +28,11 @@ * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ -#include "plugininfo.h" -#include "plugintimer.h" #include "integrationpluginfronius.h" -#include "network/networkaccessmanager.h" -#include "network/networkdevicediscovery.h" +#include "froniusdiscovery.h" +#include "plugininfo.h" + +#include #include #include @@ -56,20 +56,15 @@ void IntegrationPluginFronius::discoverThings(ThingDiscoveryInfo *info) } qCInfo(dcFronius()) << "Starting network discovery..."; - NetworkDeviceDiscoveryReply *discoveryReply = hardwareManager()->networkDeviceDiscovery()->discover(); - connect(discoveryReply, &NetworkDeviceDiscoveryReply::finished, discoveryReply, &NetworkDeviceDiscoveryReply::deleteLater); - connect(discoveryReply, &NetworkDeviceDiscoveryReply::finished, info, [=](){ + FroniusDiscovery *discovery = new FroniusDiscovery(hardwareManager()->networkManager(), hardwareManager()->networkDeviceDiscovery(), info); + connect(discovery, &FroniusDiscovery::discoveryFinished, info, [=](){ ThingDescriptors descriptors; - qCDebug(dcFronius()) << "Discovery finished. Found" << discoveryReply->networkDeviceInfos().count() << "devices"; - foreach (const NetworkDeviceInfo &networkDeviceInfo, discoveryReply->networkDeviceInfos()) { - qCDebug(dcFronius()) << networkDeviceInfo; + qCInfo(dcFronius()) << "Discovery finished. Found" << discovery->discoveryResults().count() << "devices"; + foreach (const NetworkDeviceInfo &networkDeviceInfo, discovery->discoveryResults()) { + qCInfo(dcFronius()) << "Discovered Fronius on" << networkDeviceInfo; if (networkDeviceInfo.macAddress().isNull()) continue; - // Hostname or MAC manufacturer must include Fronius - if (!(networkDeviceInfo.macAddressManufacturer().toLower().contains("fronius") || networkDeviceInfo.hostName().toLower().contains("fronius"))) - continue; - QString title; if (networkDeviceInfo.hostName().isEmpty()) { title += "Fronius Solar"; @@ -94,13 +89,14 @@ void IntegrationPluginFronius::discoverThings(ThingDiscoveryInfo *info) } ParamList params; - params << Param(connectionThingAddressParamTypeId, networkDeviceInfo.address().toString()); params << Param(connectionThingMacParamTypeId, networkDeviceInfo.macAddress()); descriptor.setParams(params); info->addThingDescriptor(descriptor); } info->finish(Thing::ThingErrorNoError); }); + + discovery->startDiscovery(); } void IntegrationPluginFronius::setupThing(ThingSetupInfo *info) @@ -110,8 +106,6 @@ void IntegrationPluginFronius::setupThing(ThingSetupInfo *info) if (thing->thingClassId() == connectionThingClassId) { - QHostAddress address(thing->paramValue(connectionThingAddressParamTypeId).toString()); - // Handle reconfigure if (m_froniusConnections.values().contains(thing)) { FroniusSolarConnection *connection = m_froniusConnections.key(thing); @@ -119,49 +113,41 @@ void IntegrationPluginFronius::setupThing(ThingSetupInfo *info) connection->deleteLater(); } + if (m_monitors.contains(thing)) { + hardwareManager()->networkDeviceDiscovery()->unregisterMonitor(m_monitors.take(thing)); + } + + // Set up depending on the available params, mac can only be filled in by discovery (ro param), + // the ip could be used as static manual config for VPN networks or WAN IP's + QHostAddress address(thing->paramValue(connectionThingAddressParamTypeId).toString()); + MacAddress mac(thing->paramValue(connectionThingMacParamTypeId).toString()); + // Create the connection - FroniusSolarConnection *connection = new FroniusSolarConnection(hardwareManager()->networkManager(), address, thing); + FroniusSolarConnection *connection = nullptr; - // Verify the version - FroniusNetworkReply *reply = connection->getVersion(); - connect(reply, &FroniusNetworkReply::finished, info, [=] { - QByteArray data = reply->networkReply()->readAll(); - if (reply->networkReply()->error() != QNetworkReply::NoError) { - qCWarning(dcFronius()) << "Network request error:" << reply->networkReply()->error() << reply->networkReply()->errorString() << reply->networkReply()->url(); - if (reply->networkReply()->error() == QNetworkReply::ContentNotFoundError) { - info->finish(Thing::ThingErrorHardwareNotAvailable, QT_TR_NOOP("The device does not reply to our requests. Please verify that the Fronius Solar API is enabled on the device.")); + if (mac.isValid() && !mac.isNull()) { + qCInfo(dcFronius()) << "Setting up network device monitor for Fronius connection using MAC address" << mac.toString(); + NetworkDeviceMonitor *monitor = hardwareManager()->networkDeviceDiscovery()->registerMonitor(mac); + m_monitors.insert(thing, monitor); + + connection = new FroniusSolarConnection(hardwareManager()->networkManager(), monitor->networkDeviceInfo().address(), thing); + connect(monitor, &NetworkDeviceMonitor::networkDeviceInfoChanged, this, [=](const NetworkDeviceInfo &networkDeviceInfo){ + qCDebug(dcFronius()) << "Network device info changed for" << thing << networkDeviceInfo; + if (networkDeviceInfo.isValid()) { + connection->setAddress(networkDeviceInfo.address()); + refreshConnection(connection); } else { - info->finish(Thing::ThingErrorHardwareNotAvailable, QT_TR_NOOP("The device is not reachable.")); + connection->setAddress(QHostAddress()); } - return; - } - - // Convert the rawdata to a JSON document - QJsonParseError error; - QJsonDocument jsonDoc = QJsonDocument::fromJson(data, &error); - if (error.error != QJsonParseError::NoError) { - qCWarning(dcFronius()) << "Failed to parse JSON data" << data << ":" << error.errorString() << data; - info->finish(Thing::ThingErrorHardwareFailure, QT_TR_NOOP("The data received from the device could not be processed because the format is unknown.")); - return; - } - - QVariantMap versionResponseMap = jsonDoc.toVariant().toMap(); - qCDebug(dcFronius()) << "Compatibility version" << versionResponseMap.value("CompatibilityRange").toString(); - - // Knwon version with broken JSON API - if (versionResponseMap.value("CompatibilityRange").toString() == "1.6-2") { - qCWarning(dcFronius()) << "The Fronius data logger has a version which is known to have a broken JSON API firmware."; - info->finish(Thing::ThingErrorHardwareFailure, QT_TR_NOOP("The firmware version 1.6-2 of this Fronius data logger contains errors preventing proper operation. Please update your Fronius device and try again.")); - return; - } - - m_froniusConnections.insert(connection, thing); - info->finish(Thing::ThingErrorNoError); - - // Update the already known states - thing->setStateValue("connected", true); - thing->setStateValue(connectionVersionStateTypeId, versionResponseMap.value("CompatibilityRange").toString()); - }); + }); + } else if (!address.isNull()) { + qCInfo(dcFronius()) << "Setting up Fronius connection based on IP address" << address.toString() << "without monitoring. Any IP changed will not be recognized and the device will be disconnected."; + connection = new FroniusSolarConnection(hardwareManager()->networkManager(), address, thing); + } else { + qCWarning(dcFronius()) << "Unable to set up thing" << thing << ", neither IP nor MAC is valid." << thing->params(); + info->finish(Thing::ThingErrorInvalidParameter, QT_TR_NOOP("Please reconfigure the device.")); + return; + } connect(connection, &FroniusSolarConnection::availableChanged, this, [=](bool available){ qCDebug(dcFronius()) << thing << "Available changed" << available; @@ -182,6 +168,53 @@ void IntegrationPluginFronius::setupThing(ThingSetupInfo *info) } }); + + if (info->isInitialSetup()) { + // Verify the version + FroniusNetworkReply *reply = connection->getVersion(); + connect(reply, &FroniusNetworkReply::finished, info, [=] { + QByteArray data = reply->networkReply()->readAll(); + if (reply->networkReply()->error() != QNetworkReply::NoError) { + qCWarning(dcFronius()) << "Network request error:" << reply->networkReply()->error() << reply->networkReply()->errorString() << reply->networkReply()->url(); + if (reply->networkReply()->error() == QNetworkReply::ContentNotFoundError) { + info->finish(Thing::ThingErrorHardwareNotAvailable, QT_TR_NOOP("The device does not reply to our requests. Please verify that the Fronius Solar API is enabled on the device.")); + } else { + info->finish(Thing::ThingErrorHardwareNotAvailable, QT_TR_NOOP("The device is not reachable.")); + } + return; + } + + // Convert the rawdata to a JSON document + QJsonParseError error; + QJsonDocument jsonDoc = QJsonDocument::fromJson(data, &error); + if (error.error != QJsonParseError::NoError) { + qCWarning(dcFronius()) << "Failed to parse JSON data" << data << ":" << error.errorString() << data; + info->finish(Thing::ThingErrorHardwareFailure, QT_TR_NOOP("The data received from the device could not be processed because the format is unknown.")); + return; + } + + QVariantMap versionResponseMap = jsonDoc.toVariant().toMap(); + qCDebug(dcFronius()) << "Compatibility version" << versionResponseMap.value("CompatibilityRange").toString(); + + // Knwon version with broken JSON API + if (versionResponseMap.value("CompatibilityRange").toString() == "1.6-2") { + qCWarning(dcFronius()) << "The Fronius data logger has a version which is known to have a broken JSON API firmware."; + info->finish(Thing::ThingErrorHardwareFailure, QT_TR_NOOP("The firmware version 1.6-2 of this Fronius data logger contains errors preventing proper operation. Please update your Fronius device and try again.")); + return; + } + + m_froniusConnections.insert(connection, thing); + info->finish(Thing::ThingErrorNoError); + + // Update the already known states + thing->setStateValue("connected", true); + thing->setStateValue(connectionVersionStateTypeId, versionResponseMap.value("CompatibilityRange").toString()); + }); + } else { + // Let the available state handle the connected state, this already worked once... + m_froniusConnections.insert(connection, thing); + info->finish(Thing::ThingErrorNoError); + } } else if ((thing->thingClassId() == inverterThingClassId || thing->thingClassId() == meterThingClassId || thing->thingClassId() == storageThingClassId)) { @@ -229,6 +262,7 @@ void IntegrationPluginFronius::postSetupThing(Thing *thing) // Refresh now FroniusSolarConnection *connection = m_froniusConnections.key(thing); if (connection) { + thing->setStateValue("connected", connection->available()); refreshConnection(connection); } } @@ -237,9 +271,15 @@ void IntegrationPluginFronius::postSetupThing(Thing *thing) void IntegrationPluginFronius::thingRemoved(Thing *thing) { if (thing->thingClassId() == connectionThingClassId) { - FroniusSolarConnection *connection = m_froniusConnections.key(thing); - m_froniusConnections.remove(connection); - connection->deleteLater(); + if (m_froniusConnections.values().contains(thing)) { + FroniusSolarConnection *connection = m_froniusConnections.key(thing); + m_froniusConnections.remove(connection); + connection->deleteLater(); + } + + if (m_monitors.contains(thing)) { + hardwareManager()->networkDeviceDiscovery()->unregisterMonitor(m_monitors.take(thing)); + } } if (myThings().filterByThingClassId(connectionThingClassId).isEmpty()) { @@ -248,7 +288,6 @@ void IntegrationPluginFronius::thingRemoved(Thing *thing) } } - void IntegrationPluginFronius::executeAction(ThingActionInfo *info) { Q_UNUSED(info) @@ -261,6 +300,11 @@ void IntegrationPluginFronius::refreshConnection(FroniusSolarConnection *connect return; } + if (connection->address().isNull()) { + qCDebug(dcFronius()) << "Connection has no IP configured yet. Skipping refresh cycle until known"; + return; + } + // Note: this call will be used to monitor the available state of the connection internally FroniusNetworkReply *reply = connection->getActiveDevices(); connect(reply, &FroniusNetworkReply::finished, this, [=]() { diff --git a/fronius/integrationpluginfronius.h b/fronius/integrationpluginfronius.h index 70271bc0..07547ffc 100644 --- a/fronius/integrationpluginfronius.h +++ b/fronius/integrationpluginfronius.h @@ -31,13 +31,11 @@ #ifndef INTEGRATIONPLUGINFRONIUS_H #define INTEGRATIONPLUGINFRONIUS_H -#include "integrations/integrationplugin.h" -#include "froniussolarconnection.h" +#include +#include +#include -#include -#include -#include -#include +#include "froniussolarconnection.h" class PluginTimer; @@ -60,6 +58,7 @@ private: PluginTimer *m_connectionRefreshTimer = nullptr; QHash m_froniusConnections; + QHash m_monitors; void refreshConnection(FroniusSolarConnection *connection); diff --git a/fronius/integrationpluginfronius.json b/fronius/integrationpluginfronius.json index e2d041d0..ed0e5935 100644 --- a/fronius/integrationpluginfronius.json +++ b/fronius/integrationpluginfronius.json @@ -22,7 +22,7 @@ "displayName": "Host address", "type": "QString", "inputType": "IPv4Address", - "defaultValue": "88.117.152.99" + "defaultValue": "" }, { "id": "2237972e-385b-4458-b5d3-1d1fb4ae8756",