Make use of network device monitor and implement kostal discovery
This commit is contained in:
parent
13a5fc9038
commit
20b414ae10
@ -29,10 +29,11 @@
|
||||
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
|
||||
|
||||
#include "integrationpluginkostal.h"
|
||||
|
||||
#include "network/networkdevicediscovery.h"
|
||||
#include "hardwaremanager.h"
|
||||
#include "plugininfo.h"
|
||||
#include "kostaldiscovery.h"
|
||||
|
||||
#include <network/networkdevicediscovery.h>
|
||||
#include <hardwaremanager.h>
|
||||
|
||||
IntegrationPluginKostal::IntegrationPluginKostal()
|
||||
{
|
||||
@ -47,49 +48,71 @@ void IntegrationPluginKostal::discoverThings(ThingDiscoveryInfo *info)
|
||||
return;
|
||||
}
|
||||
|
||||
NetworkDeviceDiscoveryReply *discoveryReply = hardwareManager()->networkDeviceDiscovery()->discover();
|
||||
connect(discoveryReply, &NetworkDeviceDiscoveryReply::finished, this, [=](){
|
||||
foreach (const NetworkDeviceInfo &networkDeviceInfo, discoveryReply->networkDeviceInfos()) {
|
||||
// Create a discovery with the info as parent for auto deleting the object once the discovery info is done
|
||||
KostalDiscovery *discovery = new KostalDiscovery(hardwareManager()->networkDeviceDiscovery(), 1502, 71, info);
|
||||
connect(discovery, &KostalDiscovery::discoveryFinished, info, [=](){
|
||||
foreach (const KostalDiscovery::KostalDiscoveryResult &result, discovery->discoveryResults()) {
|
||||
|
||||
qCDebug(dcKostal()) << "Found" << networkDeviceInfo;
|
||||
|
||||
QString title;
|
||||
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(kostalInverterThingClassId, title, description);
|
||||
ParamList params;
|
||||
params << Param(kostalInverterThingIpAddressParamTypeId, networkDeviceInfo.address().toString());
|
||||
params << Param(kostalInverterThingMacAddressParamTypeId, networkDeviceInfo.macAddress());
|
||||
descriptor.setParams(params);
|
||||
ThingDescriptor descriptor(kostalInverterThingClassId, result.manufacturerName + " " + result.productName, "Serial: " + result.serialNumber + " - " + result.networkDeviceInfo.address().toString());
|
||||
qCDebug(dcKostal()) << "Discovered:" << descriptor.title() << descriptor.description();
|
||||
|
||||
// Check if we already have set up this device
|
||||
Things existingThings = myThings().filterByParam(kostalInverterThingMacAddressParamTypeId, networkDeviceInfo.macAddress());
|
||||
Things existingThings = myThings().filterByParam(kostalInverterThingMacAddressParamTypeId, result.networkDeviceInfo.macAddress());
|
||||
if (existingThings.count() == 1) {
|
||||
qCDebug(dcKostal()) << "This connection already exists in the system:" << networkDeviceInfo;
|
||||
qCDebug(dcKostal()) << "This Kostal inverter already exists in the system:" << result.networkDeviceInfo;
|
||||
descriptor.setThingId(existingThings.first()->id());
|
||||
}
|
||||
|
||||
ParamList params;
|
||||
params << Param(kostalInverterThingMacAddressParamTypeId, result.networkDeviceInfo.macAddress());
|
||||
// Note: if we discover also the port and modbusaddress, we must fill them in from the discovery here, for now everywhere the defaults...
|
||||
descriptor.setParams(params);
|
||||
info->addThingDescriptor(descriptor);
|
||||
}
|
||||
|
||||
info->finish(Thing::ThingErrorNoError);
|
||||
});
|
||||
}
|
||||
|
||||
void IntegrationPluginKostal::startMonitoringAutoThings()
|
||||
{
|
||||
// Start the discovery process
|
||||
discovery->startDiscovery();
|
||||
|
||||
// NetworkDeviceDiscoveryReply *discoveryReply = hardwareManager()->networkDeviceDiscovery()->discover();
|
||||
// connect(discoveryReply, &NetworkDeviceDiscoveryReply::finished, this, [=](){
|
||||
// foreach (const NetworkDeviceInfo &networkDeviceInfo, discoveryReply->networkDeviceInfos()) {
|
||||
|
||||
// qCDebug(dcKostal()) << "Found" << networkDeviceInfo;
|
||||
|
||||
// QString title;
|
||||
// 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(kostalInverterThingClassId, title, description);
|
||||
// ParamList params;
|
||||
// params << Param(kostalInverterThingMacAddressParamTypeId, networkDeviceInfo.macAddress());
|
||||
// descriptor.setParams(params);
|
||||
|
||||
// // Check if we already have set up this device
|
||||
// Things existingThings = myThings().filterByParam(kostalInverterThingMacAddressParamTypeId, networkDeviceInfo.macAddress());
|
||||
// if (existingThings.count() == 1) {
|
||||
// qCDebug(dcKostal()) << "This connection already exists in the system:" << networkDeviceInfo;
|
||||
// descriptor.setThingId(existingThings.first()->id());
|
||||
// }
|
||||
|
||||
// info->addThingDescriptor(descriptor);
|
||||
// }
|
||||
|
||||
// info->finish(Thing::ThingErrorNoError);
|
||||
// });
|
||||
}
|
||||
|
||||
void IntegrationPluginKostal::setupThing(ThingSetupInfo *info)
|
||||
@ -97,124 +120,63 @@ void IntegrationPluginKostal::setupThing(ThingSetupInfo *info)
|
||||
Thing *thing = info->thing();
|
||||
qCDebug(dcKostal()) << "Setup" << thing << thing->params();
|
||||
|
||||
// Inverter (connection)
|
||||
if (thing->thingClassId() == kostalInverterThingClassId) {
|
||||
QHostAddress hostAddress = QHostAddress(thing->paramValue(kostalInverterThingIpAddressParamTypeId).toString());
|
||||
if (hostAddress.isNull()) {
|
||||
info->finish(Thing::ThingErrorInvalidParameter, QT_TR_NOOP("No IP address given"));
|
||||
|
||||
// Handle reconfigure
|
||||
if (m_kostalConnections.contains(thing)) {
|
||||
qCDebug(dcKostal()) << "Reconfiguring existing thing" << thing->name();
|
||||
m_kostalConnections.take(thing)->deleteLater();
|
||||
|
||||
if (m_monitors.contains(thing)) {
|
||||
hardwareManager()->networkDeviceDiscovery()->unregisterMonitor(m_monitors.take(thing));
|
||||
}
|
||||
}
|
||||
|
||||
MacAddress macAddress = MacAddress(thing->paramValue(kostalInverterThingMacAddressParamTypeId).toString());
|
||||
if (!macAddress.isValid()) {
|
||||
qCWarning(dcKostal()) << "The configured mac address is not valid" << thing->params();
|
||||
info->finish(Thing::ThingErrorInvalidParameter, QT_TR_NOOP("The MAC address is not known. Please reconfigure the thing."));
|
||||
return;
|
||||
}
|
||||
|
||||
uint port = thing->paramValue(kostalInverterThingPortParamTypeId).toUInt();
|
||||
quint16 slaveId = thing->paramValue(kostalInverterThingSlaveIdParamTypeId).toUInt();
|
||||
// Create the monitor
|
||||
NetworkDeviceMonitor *monitor = hardwareManager()->networkDeviceDiscovery()->registerMonitor(macAddress);
|
||||
m_monitors.insert(thing, monitor);
|
||||
|
||||
KostalModbusTcpConnection *kostalConnection = new KostalModbusTcpConnection(hostAddress, port, slaveId, this);
|
||||
connect(kostalConnection, &KostalModbusTcpConnection::initializationFinished, this, [this, thing, kostalConnection, info]{
|
||||
qCDebug(dcKostal()) << "Connection init" << kostalConnection;
|
||||
QHostAddress address = monitor->networkDeviceInfo().address();
|
||||
if (address.isNull()) {
|
||||
qCWarning(dcKostal()) << "Cannot set up thing. The host address is not known yet. Maybe it will be available in the next run...";
|
||||
hardwareManager()->networkDeviceDiscovery()->unregisterMonitor(m_monitors.take(thing));
|
||||
info->finish(Thing::ThingErrorHardwareFailure, QT_TR_NOOP("The host address is not known yet. Trying later again."));
|
||||
return;
|
||||
}
|
||||
|
||||
// FIXME: check if success
|
||||
|
||||
m_kostalConnections.insert(thing, kostalConnection);
|
||||
info->finish(Thing::ThingErrorNoError);
|
||||
|
||||
// Set connected true
|
||||
thing->setStateValue(kostalInverterConnectedStateTypeId, true);
|
||||
foreach (Thing *childThing, myThings().filterByParentId(thing->id())) {
|
||||
if (childThing->thingClassId() == kostalBatteryThingClassId) {
|
||||
childThing->setStateValue(kostalBatteryConnectedStateTypeId, true);
|
||||
} else if (childThing->thingClassId() == kostalMeterThingClassId) {
|
||||
childThing->setStateValue(kostalMeterConnectedStateTypeId, true);
|
||||
}
|
||||
}
|
||||
|
||||
connect(kostalConnection, &KostalModbusTcpConnection::totalAcPowerChanged, this, [thing](float totalAcPower){
|
||||
qCDebug(dcKostal()) << thing << "total AC power changed" << totalAcPower << "W";
|
||||
thing->setStateValue(kostalInverterCurrentPowerStateTypeId, - totalAcPower);
|
||||
});
|
||||
|
||||
connect(kostalConnection, &KostalModbusTcpConnection::totalYieldChanged, this, [thing](float totalYield){
|
||||
qCDebug(dcKostal()) << thing << "total yeald changed" << totalYield << "Wh";
|
||||
thing->setStateValue(kostalInverterTotalEnergyProducedStateTypeId, totalYield / 1000.0); // kWh
|
||||
});
|
||||
|
||||
// Current
|
||||
connect(kostalConnection, &KostalModbusTcpConnection::currentPhase1Changed, this, [thing](float currentPhase1){
|
||||
qCDebug(dcKostal()) << thing << "current phase 1 changed" << currentPhase1 << "A";
|
||||
thing->setStateValue(kostalInverterPhaseACurrentStateTypeId, currentPhase1); // A
|
||||
});
|
||||
|
||||
connect(kostalConnection, &KostalModbusTcpConnection::currentPhase2Changed, this, [thing](float currentPhase2){
|
||||
qCDebug(dcKostal()) << thing << "current phase 2 changed" << currentPhase2 << "A";
|
||||
thing->setStateValue(kostalInverterPhaseBCurrentStateTypeId, currentPhase2); // A
|
||||
});
|
||||
|
||||
connect(kostalConnection, &KostalModbusTcpConnection::currentPhase3Changed, this, [thing](float currentPhase3){
|
||||
qCDebug(dcKostal()) << thing << "current phase 3 changed" << currentPhase3 << "A";
|
||||
thing->setStateValue(kostalInverterPhaseCCurrentStateTypeId, currentPhase3); // A
|
||||
});
|
||||
|
||||
// Voltage
|
||||
connect(kostalConnection, &KostalModbusTcpConnection::voltagePhase1Changed, this, [thing](float voltagePhase1){
|
||||
qCDebug(dcKostal()) << thing << "voltage phase 1 changed" << voltagePhase1 << "V";
|
||||
thing->setStateValue(kostalInverterVoltagePhaseAStateTypeId, voltagePhase1);
|
||||
});
|
||||
|
||||
connect(kostalConnection, &KostalModbusTcpConnection::voltagePhase2Changed, this, [thing](float voltagePhase2){
|
||||
qCDebug(dcKostal()) << thing << "voltage phase 2 changed" << voltagePhase2 << "V";
|
||||
thing->setStateValue(kostalInverterVoltagePhaseBStateTypeId, voltagePhase2);
|
||||
});
|
||||
|
||||
connect(kostalConnection, &KostalModbusTcpConnection::voltagePhase3Changed, this, [thing](float voltagePhase3){
|
||||
qCDebug(dcKostal()) << thing << "voltage phase 3 changed" << voltagePhase3 << "V";
|
||||
thing->setStateValue(kostalInverterVoltagePhaseCStateTypeId, voltagePhase3);
|
||||
});
|
||||
|
||||
// Current power
|
||||
connect(kostalConnection, &KostalModbusTcpConnection::activePowerPhase1Changed, this, [thing](float activePowerPhase1){
|
||||
qCDebug(dcKostal()) << thing << "active power phase 1 changed" << activePowerPhase1 << "W";
|
||||
thing->setStateValue(kostalInverterCurrentPowerPhaseAStateTypeId, activePowerPhase1);
|
||||
});
|
||||
|
||||
connect(kostalConnection, &KostalModbusTcpConnection::activePowerPhase2Changed, this, [thing](float activePowerPhase2){
|
||||
qCDebug(dcKostal()) << thing << "active power phase 2 changed" << activePowerPhase2 << "W";
|
||||
thing->setStateValue(kostalInverterCurrentPowerPhaseBStateTypeId, activePowerPhase2);
|
||||
});
|
||||
|
||||
connect(kostalConnection, &KostalModbusTcpConnection::activePowerPhase3Changed, this, [thing](float activePowerPhase3){
|
||||
qCDebug(dcKostal()) << thing << "active power phase 3 changed" << activePowerPhase3 << "W";
|
||||
thing->setStateValue(kostalInverterCurrentPowerPhaseCStateTypeId, activePowerPhase3);
|
||||
});
|
||||
|
||||
connect(kostalConnection, &KostalModbusTcpConnection::gridFrequencyInverterChanged, this, [thing](float gridFrequencyInverter){
|
||||
qCDebug(dcKostal()) << thing << "grid frequency changed" << gridFrequencyInverter << "Hz";
|
||||
thing->setStateValue(kostalInverterFrequencyStateTypeId, gridFrequencyInverter);
|
||||
});
|
||||
|
||||
|
||||
// Update registers
|
||||
kostalConnection->update();
|
||||
});
|
||||
|
||||
connect(kostalConnection, &KostalModbusTcpConnection::connectionStateChanged, this, [this, thing, kostalConnection](bool status){
|
||||
qCDebug(dcKostal()) << "Connected changed to" << status << "for" << thing;
|
||||
if (status) {
|
||||
// Connected true will be set after successfull init
|
||||
kostalConnection->initialize();
|
||||
} else {
|
||||
thing->setStateValue(kostalInverterConnectedStateTypeId, false);
|
||||
foreach (Thing *childThing, myThings().filterByParentId(thing->id())) {
|
||||
if (childThing->thingClassId() == kostalBatteryThingClassId) {
|
||||
childThing->setStateValue(kostalBatteryConnectedStateTypeId, false);
|
||||
} else if (childThing->thingClassId() == kostalMeterThingClassId) {
|
||||
childThing->setStateValue(kostalMeterConnectedStateTypeId, false);
|
||||
}
|
||||
}
|
||||
// Clean up in case the setup gets aborted
|
||||
connect(info, &ThingSetupInfo::aborted, monitor, [=](){
|
||||
if (m_monitors.contains(thing)) {
|
||||
qCDebug(dcKostal()) << "Unregister monitor because setup has been aborted.";
|
||||
hardwareManager()->networkDeviceDiscovery()->unregisterMonitor(m_monitors.take(thing));
|
||||
}
|
||||
});
|
||||
|
||||
kostalConnection->connectDevice();
|
||||
// Wait for the monitor to be ready
|
||||
if (monitor->reachable()) {
|
||||
// Thing already reachable...let's continue with the setup
|
||||
setupKostalConnection(info);
|
||||
} else {
|
||||
qCDebug(dcKostal()) << "Waiting for the network monitor to get reachable before continue to set up the connection" << thing->name() << address.toString() << "...";
|
||||
connect(monitor, &NetworkDeviceMonitor::reachableChanged, info, [=](bool reachable){
|
||||
if (reachable) {
|
||||
qCDebug(dcKostal()) << "The monitor for thing setup" << thing->name() << "is now reachable. Continue setup...";
|
||||
setupKostalConnection(info);
|
||||
}
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Meter
|
||||
if (thing->thingClassId() == kostalMeterThingClassId) {
|
||||
// Get the parent thing and the associated connection
|
||||
Thing *connectionThing = myThings().findById(thing->parentId());
|
||||
@ -357,45 +319,47 @@ void IntegrationPluginKostal::postSetupThing(Thing *thing)
|
||||
|
||||
// Check if we have to create the meter for the Kostal inverter
|
||||
if (myThings().filterByParentId(thing->id()).filterByThingClassId(kostalMeterThingClassId).isEmpty()) {
|
||||
|
||||
qCDebug(dcKostal()) << "--> Read block \"powerMeterValues\" registers from:" << 220 << "size:" << 38;
|
||||
QModbusReply *reply = kostalConnection->readBlockPowerMeterValues();
|
||||
if (reply) {
|
||||
if (!reply->isFinished()) {
|
||||
connect(reply, &QModbusReply::finished, this, [=](){
|
||||
if (reply->error() == QModbusDevice::NoError) {
|
||||
const QModbusDataUnit unit = reply->result();
|
||||
const QVector<quint16> blockValues = unit.values();
|
||||
|
||||
bool notZero = false;
|
||||
for (int i = 0; i < blockValues.size(); i++) {
|
||||
if (blockValues.at(i) != 0) {
|
||||
notZero = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (notZero) {
|
||||
qCDebug(dcKostal()) << "There is a meter connected but not set up yet. Creating a meter...";
|
||||
// No meter thing created for this inverter, lets create one with the inverter as parent
|
||||
ThingClass meterThingClass = thingClass(kostalMeterThingClassId);
|
||||
ThingDescriptor descriptor(kostalMeterThingClassId, meterThingClass.name(), QString(), thing->id());
|
||||
// No params required, all we need is the connection
|
||||
emit autoThingsAppeared(ThingDescriptors() << descriptor);
|
||||
} else {
|
||||
qCDebug(dcKostal()) << "There is no meter connected to the inverter" << thing;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
connect(reply, &QModbusReply::errorOccurred, this, [reply] (QModbusDevice::Error error){
|
||||
qCWarning(dcKostal()) << "Modbus reply error occurred while updating block \"powerMeterValues\" registers" << error << reply->errorString();
|
||||
emit reply->finished();
|
||||
});
|
||||
}
|
||||
} else {
|
||||
if (!reply) {
|
||||
qCWarning(dcKostal()) << "Error occurred while reading block \"powerMeterValues\" registers";
|
||||
return;
|
||||
}
|
||||
|
||||
if (reply->isFinished()) {
|
||||
reply->deleteLater(); // Broadcast reply returns immediatly
|
||||
return;
|
||||
}
|
||||
|
||||
connect(reply, &QModbusReply::finished, this, [=](){
|
||||
if (reply->error() == QModbusDevice::NoError) {
|
||||
const QModbusDataUnit unit = reply->result();
|
||||
const QVector<quint16> blockValues = unit.values();
|
||||
|
||||
bool notZero = false;
|
||||
for (int i = 0; i < blockValues.size(); i++) {
|
||||
if (blockValues.at(i) != 0) {
|
||||
notZero = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (notZero) {
|
||||
qCDebug(dcKostal()) << "There is a meter connected but not set up yet. Creating a meter...";
|
||||
// No meter thing created for this inverter, lets create one with the inverter as parent
|
||||
ThingClass meterThingClass = thingClass(kostalMeterThingClassId);
|
||||
ThingDescriptor descriptor(kostalMeterThingClassId, meterThingClass.name(), QString(), thing->id());
|
||||
// No params required, all we need is the connection
|
||||
emit autoThingsAppeared(ThingDescriptors() << descriptor);
|
||||
} else {
|
||||
qCDebug(dcKostal()) << "There is no meter connected to the inverter" << thing;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
connect(reply, &QModbusReply::errorOccurred, this, [reply] (QModbusDevice::Error error){
|
||||
qCWarning(dcKostal()) << "Modbus reply error occurred while updating block \"powerMeterValues\" registers" << error << reply->errorString();
|
||||
});
|
||||
}
|
||||
|
||||
// Check if we have to create the battery for the Kostal inverter
|
||||
@ -412,6 +376,7 @@ void IntegrationPluginKostal::postSetupThing(Thing *thing)
|
||||
emit autoThingsAppeared(ThingDescriptors() << descriptor);
|
||||
}
|
||||
|
||||
// Create the update timer if not already set up
|
||||
if (!m_pluginTimer) {
|
||||
qCDebug(dcKostal()) << "Starting plugin timer...";
|
||||
m_pluginTimer = hardwareManager()->pluginTimerManager()->registerTimer(4);
|
||||
@ -433,15 +398,154 @@ void IntegrationPluginKostal::thingRemoved(Thing *thing)
|
||||
delete connection;
|
||||
}
|
||||
|
||||
// Unregister related hardware resources
|
||||
if (m_monitors.contains(thing))
|
||||
hardwareManager()->networkDeviceDiscovery()->unregisterMonitor(m_monitors.take(thing));
|
||||
|
||||
if (myThings().isEmpty() && m_pluginTimer) {
|
||||
hardwareManager()->pluginTimerManager()->unregisterTimer(m_pluginTimer);
|
||||
m_pluginTimer = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
void IntegrationPluginKostal::executeAction(ThingActionInfo *info)
|
||||
void IntegrationPluginKostal::setupKostalConnection(ThingSetupInfo *info)
|
||||
{
|
||||
info->finish(Thing::ThingErrorNoError);
|
||||
Thing *thing = info->thing();
|
||||
|
||||
QHostAddress address = m_monitors.value(thing)->networkDeviceInfo().address();
|
||||
uint port = thing->paramValue(kostalInverterThingPortParamTypeId).toUInt();
|
||||
quint16 slaveId = thing->paramValue(kostalInverterThingSlaveIdParamTypeId).toUInt();
|
||||
|
||||
qCDebug(dcKostal()) << "Setting up kostal on" << address.toString() << port << "unit ID:" << slaveId;
|
||||
KostalModbusTcpConnection *kostalConnection = new KostalModbusTcpConnection(address, port, slaveId, this);
|
||||
connect(info, &ThingSetupInfo::aborted, kostalConnection, &KostalModbusTcpConnection::deleteLater);
|
||||
|
||||
// Reconnect on monitor reachable changed
|
||||
NetworkDeviceMonitor *monitor = m_monitors.value(thing);
|
||||
connect(monitor, &NetworkDeviceMonitor::reachableChanged, thing, [=](bool reachable){
|
||||
qCDebug(dcKostal()) << "Network device monitor reachable changed for" << thing->name() << reachable;
|
||||
if (!thing->setupComplete())
|
||||
return;
|
||||
|
||||
if (reachable && !thing->stateValue("connected").toBool()) {
|
||||
kostalConnection->setHostAddress(monitor->networkDeviceInfo().address());
|
||||
kostalConnection->connectDevice();
|
||||
} else if (!reachable) {
|
||||
// Note: We disable autoreconnect explicitly and we will
|
||||
// connect the device once the monitor says it is reachable again
|
||||
kostalConnection->disconnectDevice();
|
||||
}
|
||||
});
|
||||
|
||||
connect(kostalConnection, &KostalModbusTcpConnection::initializationFinished, info, [=](bool success){
|
||||
if (!success) {
|
||||
qCWarning(dcKostal()) << "Connection init finished with errors" << thing->name() << kostalConnection->hostAddress().toString();
|
||||
hardwareManager()->networkDeviceDiscovery()->unregisterMonitor(monitor);
|
||||
kostalConnection->deleteLater();
|
||||
info->finish(Thing::ThingErrorHardwareFailure, QT_TR_NOOP("Could not initialize the communication with the wallbox."));
|
||||
return;
|
||||
}
|
||||
|
||||
qCDebug(dcKostal()) << "Connection init finished successfully" << kostalConnection;
|
||||
m_kostalConnections.insert(thing, kostalConnection);
|
||||
info->finish(Thing::ThingErrorNoError);
|
||||
|
||||
// Set connected true
|
||||
thing->setStateValue("connected", true);
|
||||
foreach (Thing *childThing, myThings().filterByParentId(thing->id())) {
|
||||
childThing->setStateValue("connected", true);
|
||||
}
|
||||
|
||||
connect(kostalConnection, &KostalModbusTcpConnection::totalAcPowerChanged, thing, [thing](float totalAcPower){
|
||||
qCDebug(dcKostal()) << thing << "total AC power changed" << totalAcPower << "W";
|
||||
thing->setStateValue(kostalInverterCurrentPowerStateTypeId, - totalAcPower);
|
||||
});
|
||||
|
||||
connect(kostalConnection, &KostalModbusTcpConnection::totalYieldChanged, thing, [thing](float totalYield){
|
||||
qCDebug(dcKostal()) << thing << "total yeald changed" << totalYield << "Wh";
|
||||
thing->setStateValue(kostalInverterTotalEnergyProducedStateTypeId, totalYield / 1000.0); // kWh
|
||||
});
|
||||
|
||||
// Current
|
||||
connect(kostalConnection, &KostalModbusTcpConnection::currentPhase1Changed, thing, [thing](float currentPhase1){
|
||||
qCDebug(dcKostal()) << thing << "current phase 1 changed" << currentPhase1 << "A";
|
||||
thing->setStateValue(kostalInverterPhaseACurrentStateTypeId, currentPhase1); // A
|
||||
});
|
||||
|
||||
connect(kostalConnection, &KostalModbusTcpConnection::currentPhase2Changed, thing, [thing](float currentPhase2){
|
||||
qCDebug(dcKostal()) << thing << "current phase 2 changed" << currentPhase2 << "A";
|
||||
thing->setStateValue(kostalInverterPhaseBCurrentStateTypeId, currentPhase2); // A
|
||||
});
|
||||
|
||||
connect(kostalConnection, &KostalModbusTcpConnection::currentPhase3Changed, thing, [thing](float currentPhase3){
|
||||
qCDebug(dcKostal()) << thing << "current phase 3 changed" << currentPhase3 << "A";
|
||||
thing->setStateValue(kostalInverterPhaseCCurrentStateTypeId, currentPhase3); // A
|
||||
});
|
||||
|
||||
// Voltage
|
||||
connect(kostalConnection, &KostalModbusTcpConnection::voltagePhase1Changed, thing, [thing](float voltagePhase1){
|
||||
qCDebug(dcKostal()) << thing << "voltage phase 1 changed" << voltagePhase1 << "V";
|
||||
thing->setStateValue(kostalInverterVoltagePhaseAStateTypeId, voltagePhase1);
|
||||
});
|
||||
|
||||
connect(kostalConnection, &KostalModbusTcpConnection::voltagePhase2Changed, thing, [thing](float voltagePhase2){
|
||||
qCDebug(dcKostal()) << thing << "voltage phase 2 changed" << voltagePhase2 << "V";
|
||||
thing->setStateValue(kostalInverterVoltagePhaseBStateTypeId, voltagePhase2);
|
||||
});
|
||||
|
||||
connect(kostalConnection, &KostalModbusTcpConnection::voltagePhase3Changed, thing, [thing](float voltagePhase3){
|
||||
qCDebug(dcKostal()) << thing << "voltage phase 3 changed" << voltagePhase3 << "V";
|
||||
thing->setStateValue(kostalInverterVoltagePhaseCStateTypeId, voltagePhase3);
|
||||
});
|
||||
|
||||
// Current power
|
||||
connect(kostalConnection, &KostalModbusTcpConnection::activePowerPhase1Changed, thing, [thing](float activePowerPhase1){
|
||||
qCDebug(dcKostal()) << thing << "active power phase 1 changed" << activePowerPhase1 << "W";
|
||||
thing->setStateValue(kostalInverterCurrentPowerPhaseAStateTypeId, activePowerPhase1);
|
||||
});
|
||||
|
||||
connect(kostalConnection, &KostalModbusTcpConnection::activePowerPhase2Changed, thing, [thing](float activePowerPhase2){
|
||||
qCDebug(dcKostal()) << thing << "active power phase 2 changed" << activePowerPhase2 << "W";
|
||||
thing->setStateValue(kostalInverterCurrentPowerPhaseBStateTypeId, activePowerPhase2);
|
||||
});
|
||||
|
||||
connect(kostalConnection, &KostalModbusTcpConnection::activePowerPhase3Changed, thing, [thing](float activePowerPhase3){
|
||||
qCDebug(dcKostal()) << thing << "active power phase 3 changed" << activePowerPhase3 << "W";
|
||||
thing->setStateValue(kostalInverterCurrentPowerPhaseCStateTypeId, activePowerPhase3);
|
||||
});
|
||||
|
||||
connect(kostalConnection, &KostalModbusTcpConnection::gridFrequencyInverterChanged, thing, [thing](float gridFrequencyInverter){
|
||||
qCDebug(dcKostal()) << thing << "grid frequency changed" << gridFrequencyInverter << "Hz";
|
||||
thing->setStateValue(kostalInverterFrequencyStateTypeId, gridFrequencyInverter);
|
||||
});
|
||||
|
||||
// Update registers
|
||||
kostalConnection->update();
|
||||
});
|
||||
|
||||
connect(kostalConnection, &KostalModbusTcpConnection::reachableChanged, thing, [this, thing, kostalConnection](bool reachable){
|
||||
qCDebug(dcKostal()) << "Reachable changed to" << reachable << "for" << thing;
|
||||
if (reachable) {
|
||||
// Connected true will be set after successfull init
|
||||
kostalConnection->initialize();
|
||||
} else {
|
||||
thing->setStateValue("connected", false);
|
||||
foreach (Thing *childThing, myThings().filterByParentId(thing->id())) {
|
||||
childThing->setStateValue("connected", false);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
connect(kostalConnection, &KostalModbusTcpConnection::initializationFinished, info, [=](bool success){
|
||||
if (success) {
|
||||
thing->setStateValue("connected", true);
|
||||
} else {
|
||||
thing->setStateValue("connected", false);
|
||||
// Try once to reconnect the device
|
||||
kostalConnection->reconnectDevice();
|
||||
}
|
||||
});
|
||||
|
||||
kostalConnection->connectDevice();
|
||||
}
|
||||
|
||||
|
||||
|
||||
@ -33,6 +33,9 @@
|
||||
|
||||
#include <plugintimer.h>
|
||||
#include <integrations/integrationplugin.h>
|
||||
#include <network/networkdevicemonitor.h>
|
||||
|
||||
#include "extern-plugininfo.h"
|
||||
|
||||
#include "kostalmodbustcpconnection.h"
|
||||
|
||||
@ -44,21 +47,19 @@ class IntegrationPluginKostal: public IntegrationPlugin
|
||||
Q_INTERFACES(IntegrationPlugin)
|
||||
|
||||
public:
|
||||
/** Constructor */
|
||||
explicit IntegrationPluginKostal();
|
||||
|
||||
void discoverThings(ThingDiscoveryInfo *info) override;
|
||||
void startMonitoringAutoThings() override;
|
||||
void setupThing(ThingSetupInfo *info) override;
|
||||
void postSetupThing(Thing *thing) override;
|
||||
void thingRemoved(Thing *thing) override;
|
||||
void executeAction(ThingActionInfo *info) override;
|
||||
|
||||
private:
|
||||
PluginTimer *m_pluginTimer = nullptr;
|
||||
QHash<Thing *, KostalModbusTcpConnection *> m_kostalConnections;
|
||||
QHash<Thing *, NetworkDeviceMonitor *> m_monitors;
|
||||
|
||||
|
||||
void setupKostalConnection(ThingSetupInfo *info);
|
||||
|
||||
};
|
||||
|
||||
|
||||
@ -16,14 +16,6 @@
|
||||
"interfaces": ["solarinverter", "connectable"],
|
||||
"providedInterfaces": [ "energymeter", "energystorage"],
|
||||
"paramTypes": [
|
||||
{
|
||||
"id": "f1c43b1e-cffe-4d30-bda0-c23ed648dd71",
|
||||
"name": "ipAddress",
|
||||
"displayName": "IP address",
|
||||
"type": "QString",
|
||||
"inputType": "IPv4Address",
|
||||
"defaultValue": "127.0.0.1"
|
||||
},
|
||||
{
|
||||
"id": "906f6099-d0e1-4297-a2b3-f8ec4482c578",
|
||||
"name":"macAddress",
|
||||
|
||||
@ -2,6 +2,8 @@
|
||||
"className": "Kostal",
|
||||
"protocol": "TCP",
|
||||
"endianness": "LittleEndian",
|
||||
"errorLimitUntilNotReachable": 20,
|
||||
"checkReachableRegister": "inverterState",
|
||||
"enums": [
|
||||
{
|
||||
"name": "ByteOrder",
|
||||
|
||||
@ -6,7 +6,9 @@ MODBUS_CONNECTIONS += kostal-registers.json
|
||||
include(../modbus.pri)
|
||||
|
||||
HEADERS += \
|
||||
integrationpluginkostal.h
|
||||
integrationpluginkostal.h \
|
||||
kostaldiscovery.h
|
||||
|
||||
SOURCES += \
|
||||
integrationpluginkostal.cpp
|
||||
integrationpluginkostal.cpp \
|
||||
kostaldiscovery.cpp
|
||||
|
||||
178
kostal/kostaldiscovery.cpp
Normal file
178
kostal/kostaldiscovery.cpp
Normal file
@ -0,0 +1,178 @@
|
||||
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
|
||||
*
|
||||
* Copyright 2013 - 2022, 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 "kostaldiscovery.h"
|
||||
#include "extern-plugininfo.h"
|
||||
|
||||
KostalDiscovery::KostalDiscovery(NetworkDeviceDiscovery *networkDeviceDiscovery, quint16 port, quint16 modbusAddress, QObject *parent) :
|
||||
QObject{parent},
|
||||
m_networkDeviceDiscovery{networkDeviceDiscovery},
|
||||
m_port{port},
|
||||
m_modbusAddress{modbusAddress}
|
||||
{
|
||||
m_gracePeriodTimer.setSingleShot(true);
|
||||
m_gracePeriodTimer.setInterval(3000);
|
||||
connect(&m_gracePeriodTimer, &QTimer::timeout, this, [this](){
|
||||
qCDebug(dcKostal()) << "Discovery: SunnyWebBox: Grace period timer triggered.";
|
||||
finishDiscovery();
|
||||
});
|
||||
}
|
||||
|
||||
void KostalDiscovery::startDiscovery()
|
||||
{
|
||||
qCInfo(dcKostal()) << "Discovery: Start searching for Kostal inverters in the network...";
|
||||
NetworkDeviceDiscoveryReply *discoveryReply = m_networkDeviceDiscovery->discover();
|
||||
|
||||
// Check any already discovered infos..
|
||||
foreach (const NetworkDeviceInfo &networkDeviceInfo, discoveryReply->networkDeviceInfos()) {
|
||||
checkNetworkDevice(networkDeviceInfo);
|
||||
}
|
||||
|
||||
// Imedialty check any new device gets discovered
|
||||
connect(discoveryReply, &NetworkDeviceDiscoveryReply::networkDeviceInfoAdded, this, &KostalDiscovery::checkNetworkDevice);
|
||||
|
||||
// Check what might be left on finished
|
||||
connect(discoveryReply, &NetworkDeviceDiscoveryReply::finished, this, [=](){
|
||||
qCDebug(dcKostal()) << "Discovery: Network discovery finished. Found" << discoveryReply->networkDeviceInfos().count() << "network devices";
|
||||
m_networkDeviceInfos = discoveryReply->networkDeviceInfos();
|
||||
qCDebug(dcKostal()) << "Discovery: Network discovery finished. Start finishing discovery...";
|
||||
// Send a report request to nework device info not sent already...
|
||||
foreach (const NetworkDeviceInfo &networkDeviceInfo, m_networkDeviceInfos) {
|
||||
if (!m_verifiedNetworkDeviceInfos.contains(networkDeviceInfo)) {
|
||||
checkNetworkDevice(networkDeviceInfo);
|
||||
}
|
||||
}
|
||||
|
||||
m_gracePeriodTimer.start();
|
||||
});
|
||||
}
|
||||
|
||||
QList<KostalDiscovery::KostalDiscoveryResult> KostalDiscovery::discoveryResults() const
|
||||
{
|
||||
return m_discoveryResults;
|
||||
}
|
||||
|
||||
void KostalDiscovery::checkNetworkDevice(const NetworkDeviceInfo &networkDeviceInfo)
|
||||
{
|
||||
// Create a kostal connection and try to initialize it.
|
||||
// Only if initialized successfully and all information have been fetched correctly from
|
||||
// the device we can assume this is what we are locking for (ip, port, modbus address, correct registers).
|
||||
// We cloud tough also filter the result only for certain software versions, manufactueres or whatever...
|
||||
|
||||
if (m_verifiedNetworkDeviceInfos.contains(networkDeviceInfo))
|
||||
return;
|
||||
|
||||
KostalModbusTcpConnection *connection = new KostalModbusTcpConnection(networkDeviceInfo.address(), m_port, m_modbusAddress, this);
|
||||
m_connections.append(connection);
|
||||
m_verifiedNetworkDeviceInfos.append(networkDeviceInfo);
|
||||
|
||||
connect(connection, &KostalModbusTcpConnection::reachableChanged, this, [=](bool reachable){
|
||||
if (!reachable) {
|
||||
// Disconnected ... done with this connection
|
||||
cleanupConnection(connection);
|
||||
return;
|
||||
}
|
||||
|
||||
// Modbus TCP connected...ok, let's try to initialize it!
|
||||
connect(connection, &KostalModbusTcpConnection::initializationFinished, this, [=](bool success){
|
||||
if (!success) {
|
||||
qCDebug(dcKostal()) << "Discovery: Initialization failed on" << networkDeviceInfo.address().toString() << "Continue...";;
|
||||
cleanupConnection(connection);
|
||||
return;
|
||||
}
|
||||
KostalDiscoveryResult result;
|
||||
result.productName = connection->productName();
|
||||
result.manufacturerName = connection->inverterManufacturer();
|
||||
result.serialNumber = connection->inverterSerialNumber1();
|
||||
result.articleNumber = connection->inverterArticleNumber();
|
||||
result.softwareVersionIoController = connection->softwareVersionIoController();
|
||||
result.softwareVersionMainController = connection->softwareVersionMainController();
|
||||
result.networkDeviceInfo = networkDeviceInfo;
|
||||
m_discoveryResults.append(result);
|
||||
|
||||
qCDebug(dcKostal()) << "Discovery: --> Found" << result.manufacturerName << result.productName
|
||||
<< "Article:" << result.articleNumber
|
||||
<< "Serial number:" << result.serialNumber
|
||||
<< "Software version main controller:" << result.softwareVersionMainController
|
||||
<< "Software version IO controller:" << result.softwareVersionIoController
|
||||
<< result.networkDeviceInfo;
|
||||
|
||||
|
||||
// Done with this connection
|
||||
cleanupConnection(connection);
|
||||
});
|
||||
|
||||
if (!connection->initialize()) {
|
||||
qCDebug(dcKostal()) << "Discovery: Unable to initialize connection on" << networkDeviceInfo.address().toString() << "Continue...";;
|
||||
cleanupConnection(connection);
|
||||
}
|
||||
|
||||
// Initializing...
|
||||
});
|
||||
|
||||
// If we get any error...skip this host...
|
||||
connect(connection, &KostalModbusTcpConnection::connectionErrorOccurred, this, [=](QModbusDevice::Error error){
|
||||
if (error != QModbusDevice::NoError) {
|
||||
qCDebug(dcKostal()) << "Discovery: Connection error on" << networkDeviceInfo.address().toString() << "Continue...";;
|
||||
cleanupConnection(connection);
|
||||
}
|
||||
});
|
||||
|
||||
// If check reachability failed...skip this host...
|
||||
connect(connection, &KostalModbusTcpConnection::checkReachabilityFailed, this, [=](){
|
||||
qCDebug(dcKostal()) << "Discovery: Check reachability failed on" << networkDeviceInfo.address().toString() << "Continue...";;
|
||||
cleanupConnection(connection);
|
||||
});
|
||||
|
||||
// Try to connect, maybe it works, maybe not...
|
||||
connection->connectDevice();
|
||||
}
|
||||
|
||||
void KostalDiscovery::cleanupConnection(KostalModbusTcpConnection *connection)
|
||||
{
|
||||
m_connections.removeAll(connection);
|
||||
connection->disconnectDevice();
|
||||
connection->deleteLater();
|
||||
}
|
||||
|
||||
void KostalDiscovery::finishDiscovery()
|
||||
{
|
||||
qint64 durationMilliSeconds = QDateTime::currentMSecsSinceEpoch() - m_startDateTime.toMSecsSinceEpoch();
|
||||
|
||||
// Cleanup any leftovers...we don't care any more
|
||||
foreach (KostalModbusTcpConnection *connection, m_connections)
|
||||
cleanupConnection(connection);
|
||||
|
||||
qCInfo(dcKostal()) << "Discovery: Finished the discovery process. Found" << m_discoveryResults.count()
|
||||
<< "Kostal Inverters in" << QTime::fromMSecsSinceStartOfDay(durationMilliSeconds).toString("mm:ss.zzz");
|
||||
m_gracePeriodTimer.stop();
|
||||
|
||||
emit discoveryFinished();
|
||||
}
|
||||
84
kostal/kostaldiscovery.h
Normal file
84
kostal/kostaldiscovery.h
Normal file
@ -0,0 +1,84 @@
|
||||
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
|
||||
*
|
||||
* Copyright 2013 - 2022, 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 KOSTALDISCOVERY_H
|
||||
#define KOSTALDISCOVERY_H
|
||||
|
||||
#include <QObject>
|
||||
#include <QTimer>
|
||||
|
||||
#include <network/networkdevicediscovery.h>
|
||||
|
||||
#include "kostalmodbustcpconnection.h"
|
||||
|
||||
class KostalDiscovery : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit KostalDiscovery(NetworkDeviceDiscovery *networkDeviceDiscovery, quint16 port = 1502, quint16 modbusAddress = 71, QObject *parent = nullptr);
|
||||
typedef struct KostalDiscoveryResult {
|
||||
QString productName;
|
||||
QString manufacturerName;
|
||||
QString serialNumber;
|
||||
QString articleNumber;
|
||||
QString softwareVersionMainController;
|
||||
QString softwareVersionIoController;
|
||||
NetworkDeviceInfo networkDeviceInfo;
|
||||
} KostalDiscoveryResult;
|
||||
|
||||
void startDiscovery();
|
||||
|
||||
QList<KostalDiscoveryResult> discoveryResults() const;
|
||||
|
||||
signals:
|
||||
void discoveryFinished();
|
||||
|
||||
private:
|
||||
NetworkDeviceDiscovery *m_networkDeviceDiscovery = nullptr;
|
||||
quint16 m_port;
|
||||
quint16 m_modbusAddress;
|
||||
|
||||
QTimer m_gracePeriodTimer;
|
||||
QDateTime m_startDateTime;
|
||||
|
||||
NetworkDeviceInfos m_networkDeviceInfos;
|
||||
NetworkDeviceInfos m_verifiedNetworkDeviceInfos;
|
||||
|
||||
QList<KostalModbusTcpConnection *> m_connections;
|
||||
|
||||
QList<KostalDiscoveryResult> m_discoveryResults;
|
||||
|
||||
void checkNetworkDevice(const NetworkDeviceInfo &networkDeviceInfo);
|
||||
void cleanupConnection(KostalModbusTcpConnection *connection);
|
||||
|
||||
void finishDiscovery();
|
||||
};
|
||||
|
||||
#endif // KOSTALDISCOVERY_H
|
||||
@ -454,6 +454,7 @@ QUuid ModbusTCPMaster::writeHoldingRegister(uint slaveAddress, uint registerAddr
|
||||
void ModbusTCPMaster::onModbusErrorOccurred(QModbusDevice::Error error)
|
||||
{
|
||||
qCWarning(dcModbusTcpMaster()) << "An error occurred" << error;
|
||||
emit connectionErrorOccurred(error);
|
||||
}
|
||||
|
||||
void ModbusTCPMaster::onModbusStateChanged(QModbusDevice::State state)
|
||||
|
||||
@ -104,6 +104,7 @@ private slots:
|
||||
|
||||
signals:
|
||||
void connectionStateChanged(bool status);
|
||||
void connectionErrorOccurred(QModbusDevice::Error error);
|
||||
|
||||
void writeRequestExecuted(const QUuid &requestId, bool success);
|
||||
void writeRequestError(const QUuid &requestId, const QString &error);
|
||||
|
||||
Reference in New Issue
Block a user