Keba: Update discovery mechanism

master
Simon Stürz 2021-11-29 14:16:18 +01:00
parent 9e1780c5c4
commit e42322694f
6 changed files with 289 additions and 65 deletions

View File

@ -30,12 +30,13 @@
#include "plugininfo.h"
#include "integrationpluginkeba.h"
#include "network/networkdevicediscovery.h"
#include <QJsonDocument>
#include <QUdpSocket>
#include <QTimeZone>
#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<KeContact *>(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);

View File

@ -31,8 +31,10 @@
#ifndef INTEGRATIONPLUGINKEBA_H
#define INTEGRATIONPLUGINKEBA_H
#include "integrations/integrationplugin.h"
#include "plugintimer.h"
#include <integrations/integrationplugin.h>
#include <plugintimer.h>
#include <network/networkdevicediscovery.h>
#include "kecontact.h"
#include "kecontactdatalayer.h"
@ -41,6 +43,7 @@
#include <QDateTime>
#include <QUdpSocket>
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<ThingId, KeContact *> m_kebaDevices;
QHash<ThingId, int> m_lastSessionId;
QHash<QUuid, ThingActionInfo *> 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);

View File

@ -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": [

View File

@ -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

136
keba/kebadiscovery.cpp Normal file
View File

@ -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 <https://www.gnu.org/licenses/>.
*
* 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 <QJsonDocument>
#include <network/networkdevicediscovery.h>
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::KebaDiscoveryResult> 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;
}

73
keba/kebadiscovery.h Normal file
View File

@ -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 <https://www.gnu.org/licenses/>.
*
* 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 <QTimer>
#include <QObject>
#include <network/networkdeviceinfos.h>
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<KebaDiscoveryResult> discoveryResults() const;
signals:
void discoveryFinished();
private:
KeContactDataLayer *m_kebaDataLayer = nullptr;
NetworkDeviceDiscovery *m_networkDeviceDiscovery = nullptr;
QTimer m_responseTimer;
NetworkDeviceInfos m_networkDeviceInfos;
QList<KebaDiscoveryResult> m_results;
bool alreadyDiscovered(const QHostAddress &address);
};
#endif // KEBADISCOVERY_H