diff --git a/keba/integrationpluginkeba.cpp b/keba/integrationpluginkeba.cpp index 80209bb8..9c913e08 100644 --- a/keba/integrationpluginkeba.cpp +++ b/keba/integrationpluginkeba.cpp @@ -30,12 +30,13 @@ #include "plugininfo.h" #include "integrationpluginkeba.h" -#include "network/networkdevicediscovery.h" #include #include #include +#include "kebadiscovery.h" + IntegrationPluginKeba::IntegrationPluginKeba() { @@ -48,49 +49,50 @@ void IntegrationPluginKeba::init() void IntegrationPluginKeba::discoverThings(ThingDiscoveryInfo *info) { + + // Init data layer if not already created + if (!m_kebaDataLayer){ + qCDebug(dcKeba()) << "Creating new Keba data layer..."; + m_kebaDataLayer= new KeContactDataLayer(this); + if (!m_kebaDataLayer->init()) { + m_kebaDataLayer->deleteLater(); + m_kebaDataLayer = nullptr; + qCWarning(dcKeba()) << "Failed to create Keba data layer..."; + info->finish(Thing::ThingErrorHardwareFailure); + return; + } + } + if (info->thingClassId() == wallboxThingClassId) { - qCDebug(dcKeba()) << "Discovering Keba Wallbox..."; - NetworkDeviceDiscoveryReply *discoveryReply = hardwareManager()->networkDeviceDiscovery()->discover(); - connect(discoveryReply, &NetworkDeviceDiscoveryReply::finished, this, [=](){ - ThingDescriptors descriptors; - qCDebug(dcKeba()) << "Discovery finished. Found" << discoveryReply->networkDeviceInfos().count() << "devices"; - foreach (const NetworkDeviceInfo &networkDeviceInfo, discoveryReply->networkDeviceInfos()) { - if (!networkDeviceInfo.macAddressManufacturer().contains("keba", Qt::CaseSensitivity::CaseInsensitive)) - continue; + // Create a discovery with the info as parent for auto deleting the object + KebaDiscovery *discovery = new KebaDiscovery(m_kebaDataLayer, hardwareManager()->networkDeviceDiscovery(), info); + connect(discovery, &KebaDiscovery::discoveryFinished, info, [=](){ + foreach (const KebaDiscovery::KebaDiscoveryResult &result, discovery->discoveryResults()) { - qCDebug(dcKeba()) << " - Keba Wallbox" << networkDeviceInfo; - QString title = "Keba Wallbox "; - if (networkDeviceInfo.hostName().isEmpty()) { - title += "(" + networkDeviceInfo.address().toString() + ")"; - } else { - title += networkDeviceInfo.hostName() + " (" + networkDeviceInfo.address().toString() + ")"; - } - - QString description; - if (networkDeviceInfo.macAddressManufacturer().isEmpty()) { - description = networkDeviceInfo.macAddress(); - } else { - description = networkDeviceInfo.macAddress() + " (" + networkDeviceInfo.macAddressManufacturer() + ")"; - } - - ThingDescriptor descriptor(wallboxThingClassId, title, description); + ThingDescriptor descriptor(wallboxThingClassId, "Keba " + result.product, "Serial: " + result.serialNumber + " - " + result.networkDeviceInfo.address().toString()); // Check if we already have set up this device - Things existingThings = myThings().filterByParam(wallboxThingMacAddressParamTypeId, networkDeviceInfo.macAddress()); + Things existingThings = myThings().filterByParam(wallboxThingMacAddressParamTypeId, result.networkDeviceInfo.macAddress()); if (existingThings.count() == 1) { - qCDebug(dcKeba()) << "This wallbox already exists in the system!" << networkDeviceInfo; + qCDebug(dcKeba()) << "This wallbox already exists in the system!" << result.networkDeviceInfo; descriptor.setThingId(existingThings.first()->id()); } ParamList params; - params << Param(wallboxThingMacAddressParamTypeId, networkDeviceInfo.macAddress()); - params << Param(wallboxThingIpAddressParamTypeId, networkDeviceInfo.address().toString()); + params << Param(wallboxThingMacAddressParamTypeId, result.networkDeviceInfo.macAddress()); + params << Param(wallboxThingIpAddressParamTypeId, result.networkDeviceInfo.address().toString()); + params << Param(wallboxThingModelParamTypeId, result.product); + params << Param(wallboxThingSerialNumberParamTypeId, result.serialNumber); descriptor.setParams(params); info->addThingDescriptor(descriptor); } info->finish(Thing::ThingErrorNoError); }); + + // Start the discovery process + discovery->startDiscovery(); + } else { qCWarning(dcKeba()) << "Could not discover things because of unhandled thing class id" << info->thingClassId().toString(); info->finish(Thing::ThingErrorThingClassNotFound); @@ -143,6 +145,9 @@ void IntegrationPluginKeba::setupThing(ThingSetupInfo *info) connect(keba, &KeContact::report1XXReceived, this, &IntegrationPluginKeba::onReport1XXReceived); connect(keba, &KeContact::broadcastReceived, this, &IntegrationPluginKeba::onBroadcastReceived); + // TODO: first test the ip, verify serial number if responds + // if no response, rediscover, reassign ip in case if changes + connect(keba, &KeContact::reportOneReceived, info, [info, this, keba] (const KeContact::ReportOne &report) { Thing *thing = info->thing(); @@ -155,8 +160,6 @@ void IntegrationPluginKeba::setupThing(ThingSetupInfo *info) thing->setStateValue(wallboxConnectedStateTypeId, true); thing->setStateValue(wallboxFirmwareStateTypeId, report.firmware); - thing->setStateValue(wallboxSerialnumberStateTypeId, report.serialNumber); - thing->setStateValue(wallboxModelStateTypeId, report.product); thing->setStateValue(wallboxUptimeStateTypeId, report.seconds / 60); m_kebaDevices.insert(thing->id(), keba); @@ -358,6 +361,11 @@ void IntegrationPluginKeba::searchNetworkDevices() }); } +void IntegrationPluginKeba::onDiscoveryWaitUpdResponseTimeout() +{ + +} + void IntegrationPluginKeba::onConnectionChanged(bool status) { KeContact *keba = static_cast(sender()); @@ -402,7 +410,7 @@ void IntegrationPluginKeba::onReportTwoReceived(const KeContact::ReportTwo &repo if (!thing) return; - qCDebug(dcKeba()) << "Report 2 received for" << thing->name() << "Serial number:" << thing->stateValue(wallboxSerialnumberStateTypeId).toString(); + qCDebug(dcKeba()) << "Report 2 received for" << thing->name() << "Serial number:" << thing->paramValue(wallboxThingSerialNumberParamTypeId).toString(); qCDebug(dcKeba()) << " - State:" << reportTwo.state; qCDebug(dcKeba()) << " - Error 1:" << reportTwo.error1; qCDebug(dcKeba()) << " - Error 2:" << reportTwo.error2; @@ -422,7 +430,7 @@ void IntegrationPluginKeba::onReportTwoReceived(const KeContact::ReportTwo &repo qCDebug(dcKeba()) << " - Serial number:" << reportTwo.serialNumber; qCDebug(dcKeba()) << " - Uptime:" << reportTwo.seconds/60 << "[min]"; - if (reportTwo.serialNumber == thing->stateValue(wallboxSerialnumberStateTypeId).toString()) { + if (reportTwo.serialNumber == thing->paramValue(wallboxThingSerialNumberParamTypeId).toString()) { setDeviceState(thing, reportTwo.state); setDevicePlugState(thing, reportTwo.plugState); @@ -458,7 +466,7 @@ void IntegrationPluginKeba::onReportThreeReceived(const KeContact::ReportThree & if (!thing) return; - qCDebug(dcKeba()) << "Report 3 received for" << thing->name() << "Serial number:" << thing->stateValue(wallboxSerialnumberStateTypeId).toString(); + qCDebug(dcKeba()) << "Report 3 received for" << thing->name() << "Serial number:" << thing->paramValue(wallboxThingSerialNumberParamTypeId).toString(); qCDebug(dcKeba()) << " - Current phase 1:" << reportThree.currentPhase1 << "[A]"; qCDebug(dcKeba()) << " - Current phase 2:" << reportThree.currentPhase2 << "[A]"; qCDebug(dcKeba()) << " - Current phase 3:" << reportThree.currentPhase3 << "[A]"; @@ -471,7 +479,7 @@ void IntegrationPluginKeba::onReportThreeReceived(const KeContact::ReportThree & qCDebug(dcKeba()) << " - Serial number" << reportThree.serialNumber; qCDebug(dcKeba()) << " - Uptime" << reportThree.seconds / 60 << "[min]"; - if (reportThree.serialNumber == thing->stateValue(wallboxSerialnumberStateTypeId).toString()) { + if (reportThree.serialNumber == thing->paramValue(wallboxThingSerialNumberParamTypeId).toString()) { thing->setStateValue(wallboxCurrentPhaseAEventTypeId, reportThree.currentPhase1); thing->setStateValue(wallboxCurrentPhaseBEventTypeId, reportThree.currentPhase2); thing->setStateValue(wallboxCurrentPhaseCEventTypeId, reportThree.currentPhase3); @@ -509,7 +517,7 @@ void IntegrationPluginKeba::onReport1XXReceived(int reportNumber, const KeContac if (!thing) return; - qCDebug(dcKeba()) << "Report" << reportNumber << "received for" << thing->name() << "Serial number:" << thing->stateValue(wallboxSerialnumberStateTypeId).toString(); + qCDebug(dcKeba()) << "Report" << reportNumber << "received for" << thing->name() << "Serial number:" << thing->paramValue(wallboxThingSerialNumberParamTypeId).toString(); qCDebug(dcKeba()) << " - Session Id" << report.sessionId; qCDebug(dcKeba()) << " - Curr HW" << report.currHW; qCDebug(dcKeba()) << " - Energy start" << report.startEnergy; @@ -534,7 +542,7 @@ void IntegrationPluginKeba::onReport1XXReceived(int reportNumber, const KeContac } else if (reportNumber == 101) { // Report 101 is the lastest finished session - if (report.serialNumber == thing->stateValue(wallboxSerialnumberStateTypeId).toString()) { + if (report.serialNumber == thing->paramValue(wallboxThingSerialNumberParamTypeId).toString()) { if (!m_lastSessionId.contains(thing->id())) { // This happens after reboot m_lastSessionId.insert(thing->id(), report.sessionId); diff --git a/keba/integrationpluginkeba.h b/keba/integrationpluginkeba.h index 03abbcc6..aa982bfa 100644 --- a/keba/integrationpluginkeba.h +++ b/keba/integrationpluginkeba.h @@ -31,8 +31,10 @@ #ifndef INTEGRATIONPLUGINKEBA_H #define INTEGRATIONPLUGINKEBA_H -#include "integrations/integrationplugin.h" -#include "plugintimer.h" +#include +#include +#include + #include "kecontact.h" #include "kecontactdatalayer.h" @@ -41,6 +43,7 @@ #include #include + class IntegrationPluginKeba : public IntegrationPlugin { Q_OBJECT @@ -64,12 +67,10 @@ public: private: PluginTimer *m_updateTimer = nullptr; PluginTimer *m_reconnectTimer = nullptr; - KeContactDataLayer *m_kebaDataLayer = nullptr; QHash m_kebaDevices; QHash m_lastSessionId; - QHash m_asyncActions; void setDeviceState(Thing *device, KeContact::State state); @@ -78,6 +79,8 @@ private: void searchNetworkDevices(); private slots: + void onDiscoveryWaitUpdResponseTimeout(); + void onConnectionChanged(bool status); void onCommandExecuted(QUuid requestId, bool success); void onReportTwoReceived(const KeContact::ReportTwo &reportTwo); diff --git a/keba/integrationpluginkeba.json b/keba/integrationpluginkeba.json index 9cf84009..ab3c8c10 100644 --- a/keba/integrationpluginkeba.json +++ b/keba/integrationpluginkeba.json @@ -31,6 +31,24 @@ "inputType": "TextLine", "defaultValue":"", "readOnly": true + }, + { + "id": "45255155-318b-4204-8ce6-2c106a56286d", + "name": "serialNumber", + "displayName": "Serial number", + "type": "QString", + "inputType": "TextLine", + "defaultValue":"", + "readOnly": true + }, + { + "id": "a996c698-4831-4977-8979-f76f78ac7da8", + "name": "model", + "displayName": "Product name", + "type": "QString", + "inputType": "TextLine", + "defaultValue":"", + "readOnly": true } ], "stateTypes": [ @@ -43,30 +61,6 @@ "defaultValue": false, "cached": false }, - { - "id": "c3fca233-95b9-4948-88c6-4c0f13cf53b1", - "name": "model", - "displayName": "Model", - "displayNameEvent": "Model changed", - "type": "QString", - "defaultValue": "Unknown" - }, - { - "id": "e941ace5-fb7f-4dc2-b3f2-188233f4e934", - "name": "firmware", - "displayName": "Firmware", - "displayNameEvent": "Firmware changed", - "type": "QString", - "defaultValue": "" - }, - { - "id": "9a1b4316-ce01-4cd3-890f-a8c94b8b5029", - "name": "serialnumber", - "displayName": "Serial number", - "displayNameEvent": "Serial number changed", - "type": "QString", - "defaultValue": "" - }, { "id": "83ed0774-2a91-434d-b03c-d920d02f2981", "name": "power", @@ -328,6 +322,14 @@ "writable": true, "type": "bool", "defaultValue": false + }, + { + "id": "e941ace5-fb7f-4dc2-b3f2-188233f4e934", + "name": "firmware", + "displayName": "Firmware", + "displayNameEvent": "Firmware changed", + "type": "QString", + "defaultValue": "" } ], "actionTypes": [ diff --git a/keba/keba.pro b/keba/keba.pro index ea4239f2..e8bc1164 100644 --- a/keba/keba.pro +++ b/keba/keba.pro @@ -6,10 +6,12 @@ TARGET = $$qtLibraryTarget(nymea_integrationpluginkeba) SOURCES += \ integrationpluginkeba.cpp \ + kebadiscovery.cpp \ kecontact.cpp \ kecontactdatalayer.cpp HEADERS += \ integrationpluginkeba.h \ + kebadiscovery.h \ kecontact.h \ kecontactdatalayer.h diff --git a/keba/kebadiscovery.cpp b/keba/kebadiscovery.cpp new file mode 100644 index 00000000..08a1aadd --- /dev/null +++ b/keba/kebadiscovery.cpp @@ -0,0 +1,136 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* Copyright 2013 - 2021, 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 "kebadiscovery.h" +#include "kecontactdatalayer.h" +#include "extern-plugininfo.h" + +#include +#include + +KebaDiscovery::KebaDiscovery(KeContactDataLayer *kebaDataLayer, NetworkDeviceDiscovery *networkDeviceDiscovery, QObject *parent) : + QObject(parent), + m_kebaDataLayer(kebaDataLayer), + m_networkDeviceDiscovery(networkDeviceDiscovery) +{ + // Timer for waiting if network devices responded to the "report 1 request" + m_responseTimer.setInterval(5000); + m_responseTimer.setSingleShot(true); + connect(&m_responseTimer, &QTimer::timeout, this, [=](){ + qCDebug(dcKeba()) << "Discovery: Report response timeout. Found" << m_results.count() << "Keba Wallbox"; + emit discoveryFinished(); + }); + + // Read data from the keba data layer and verify if it is a keba report + connect (m_kebaDataLayer, &KeContactDataLayer::datagramReceived, this, [=](const QHostAddress &address, const QByteArray &datagram){ + + // Just continue if this is a new address we have no result for + if (alreadyDiscovered(address)) { + qCDebug(dcKeba()) << "Discovery: Skipping datagram from already discovered Keba on" << address.toString(); + return; + } + + // Try to convert the received data to a json document + QJsonParseError error; + QJsonDocument jsonDoc = QJsonDocument::fromJson(datagram, &error); + if (error.error != QJsonParseError::NoError) { + qCWarning(dcKeba()) << "Discovery: Received data from the keba data link but failed to parse the data as JSON:" << datagram << ":" << error.errorString(); + return; + } + + // Verify JSON data + QVariantMap dataMap = jsonDoc.toVariant().toMap(); + if (!dataMap.contains("ID") || !dataMap.contains("Serial") || !dataMap.contains("Product") || !dataMap.contains("Firmware")) { + qCDebug(dcKeba()) << "Discovery: Received valid JSON data on data layer but they don't seem to be what we are listening for:" << qUtf8Printable(jsonDoc.toJson()); + return; + } + + if (dataMap.value("ID").toInt() != 1) { + qCDebug(dcKeba()) << "Discovery: Received valid Keba JSON data on data layer but this is not a report 1 message:" << qUtf8Printable(jsonDoc.toJson()); + return; + } + + // We have received a report 1 datagram, let's add it to the result + foreach (const NetworkDeviceInfo &networkDeviceInfo, m_networkDeviceInfos) { + if (networkDeviceInfo.address() == address) { + KebaDiscoveryResult result; + result.networkDeviceInfo = networkDeviceInfo; + result.product = dataMap.value("Product").toString(); + result.serialNumber = dataMap.value("Serial").toString(); + result.firmwareVersion = dataMap.value("Firmware").toString(); + m_results.append(result); + qCDebug(dcKeba()) << "Discovery: -->" << networkDeviceInfo.address().toString() << networkDeviceInfo.macAddress() << result.product << result.serialNumber << result.firmwareVersion; + } + } + }); + +} + +KebaDiscovery::~KebaDiscovery() +{ + qCDebug(dcKeba()) << "Discovery: Destroying object."; +} + +void KebaDiscovery::startDiscovery() +{ + // Clean up + m_networkDeviceInfos.clear(); + m_results.clear(); + + qCDebug(dcKeba()) << "Discovery: Start discovering Keba Wallboxs..."; + NetworkDeviceDiscoveryReply *discoveryReply = m_networkDeviceDiscovery->discover(); + connect(discoveryReply, &NetworkDeviceDiscoveryReply::finished, this, [=](){ + qCDebug(dcKeba()) << "Discovery: Network discovery finished. Found" << discoveryReply->networkDeviceInfos().count() << "network devices"; + m_networkDeviceInfos = discoveryReply->networkDeviceInfos(); + + // Send a report 1 request to all discovered network devices and see which one responds + qCDebug(dcKeba()) << "Discovery: Start sending \"report 1\" request to all discovered network devices"; + foreach (const NetworkDeviceInfo &networkDeviceInfo, m_networkDeviceInfos) + m_kebaDataLayer->write(networkDeviceInfo.address(), QByteArray("report 1\n")); + + m_responseTimer.start(); + }); +} + +QList KebaDiscovery::discoveryResults() const +{ + return m_results; +} + +bool KebaDiscovery::alreadyDiscovered(const QHostAddress &address) +{ + foreach (const KebaDiscoveryResult &result, m_results) { + if (result.networkDeviceInfo.address() == address) { + return true; + } + } + + return false; +} diff --git a/keba/kebadiscovery.h b/keba/kebadiscovery.h new file mode 100644 index 00000000..d7556944 --- /dev/null +++ b/keba/kebadiscovery.h @@ -0,0 +1,73 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* Copyright 2013 - 2021, 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 KEBADISCOVERY_H +#define KEBADISCOVERY_H + +#include +#include + +#include + +class KeContactDataLayer; +class NetworkDeviceDiscovery; + +class KebaDiscovery : public QObject +{ + Q_OBJECT +public: + typedef struct KebaDiscoveryResult { + QString product; + QString serialNumber; + QString firmwareVersion; + NetworkDeviceInfo networkDeviceInfo; + } KebaDiscoveryResult; + + explicit KebaDiscovery(KeContactDataLayer *kebaDataLayer, NetworkDeviceDiscovery *networkDeviceDiscovery, QObject *parent = nullptr); + ~KebaDiscovery(); + + void startDiscovery(); + + QList discoveryResults() const; + +signals: + void discoveryFinished(); + +private: + KeContactDataLayer *m_kebaDataLayer = nullptr; + NetworkDeviceDiscovery *m_networkDeviceDiscovery = nullptr; + QTimer m_responseTimer; + NetworkDeviceInfos m_networkDeviceInfos; + QList m_results; + + bool alreadyDiscovered(const QHostAddress &address); +}; + +#endif // KEBADISCOVERY_H