From 648f30c4abc6c5ee9e86d16d5ce358f7b866bec5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20St=C3=BCrz?= Date: Thu, 2 Oct 2025 12:17:38 +0200 Subject: [PATCH] EVerest: Improve connection initialization The initialization has now a retry logic if there are no data vailable yet. Retry up to 4 times to create a localhost instance. The max charging current will be ignored if a 0 value has been received. --- everest/integrationplugineverest.cpp | 26 ++- everest/integrationplugineverest.h | 2 + everest/jsonrpc/everestevse.cpp | 191 +++++++++----------- everest/jsonrpc/everestevse.h | 3 - everest/jsonrpc/everestjsonrpcclient.cpp | 42 ++++- everest/jsonrpc/everestjsonrpcclient.h | 4 +- everest/jsonrpc/everestjsonrpcinterface.cpp | 10 +- everest/jsonrpc/everestjsonrpcreply.cpp | 15 ++ everest/jsonrpc/everestjsonrpcreply.h | 10 + 9 files changed, 176 insertions(+), 127 deletions(-) diff --git a/everest/integrationplugineverest.cpp b/everest/integrationplugineverest.cpp index 3f17c333..3a80c9a8 100644 --- a/everest/integrationplugineverest.cpp +++ b/everest/integrationplugineverest.cpp @@ -92,13 +92,25 @@ void IntegrationPluginEverest::startMonitoringAutoThings() client->disconnectFromServer(); client->deleteLater(); + + // Disable any auto setup retry logic... + m_autodetectCounter = m_autodetectCounterLimit; } }); - connect(client, &EverestJsonRpcClient::connectionErrorOccurred, this, [client](){ - qCDebug(dcEverest()) << "AutoSetup: The connection to" << client->serverUrl().toString() << "failed"; - client->disconnectFromServer(); - client->deleteLater(); + connect(client, &EverestJsonRpcClient::connectionErrorOccurred, this, [this, client, url](){ + m_autodetectCounter++; + + if (m_autodetectCounter <= m_autodetectCounterLimit) { + qCDebug(dcEverest()) << "AutoSetup: The connection to" << client->serverUrl().toString() << "failed. Retry" << m_autodetectCounter << "/" << m_autodetectCounterLimit << "in 15 seconds..."; + QTimer::singleShot(15000, client, [client, url](){ + client->connectToServer(url); + }); + } else { + qCDebug(dcEverest()) << "AutoSetup: The connection to" << client->serverUrl().toString() << "failed after" << m_autodetectCounterLimit << "retries. Stopping AutoSetup."; + client->disconnectFromServer(); + client->deleteLater(); + } }); client->connectToServer(url); @@ -333,6 +345,8 @@ void IntegrationPluginEverest::discoverThings(ThingDiscoveryInfo *info) jsonRpcDiscovery->start(); return; } + + info->finish(Thing::ThingErrorUnsupportedFeature); } void IntegrationPluginEverest::setupThing(ThingSetupInfo *info) @@ -374,10 +388,6 @@ void IntegrationPluginEverest::setupThing(ThingSetupInfo *info) return; } else if (thing->thingClassId() == everestConnectionThingClassId) { - QHostAddress address(thing->paramValue(everestConnectionThingAddressParamTypeId).toString()); - MacAddress macAddress(thing->paramValue(everestConnectionThingMacAddressParamTypeId).toString()); - QString hostName(thing->paramValue(everestConnectionThingHostNameParamTypeId).toString()); - quint16 port = thing->paramValue(everestConnectionThingPortParamTypeId).toUInt(); EverestConnection *connection = nullptr; diff --git a/everest/integrationplugineverest.h b/everest/integrationplugineverest.h index 58e5e56b..66e51387 100644 --- a/everest/integrationplugineverest.h +++ b/everest/integrationplugineverest.h @@ -59,6 +59,8 @@ public: void executeAction(ThingActionInfo *info) override; private: + int m_autodetectCounter = 0; + int m_autodetectCounterLimit = 4; bool m_useMqtt = false; QList m_everstMqttClients; QHash m_thingClients; diff --git a/everest/jsonrpc/everestevse.cpp b/everest/jsonrpc/everestevse.cpp index 51593939..a12100c9 100644 --- a/everest/jsonrpc/everestevse.cpp +++ b/everest/jsonrpc/everestevse.cpp @@ -104,131 +104,110 @@ EverestJsonRpcReply *EverestEvse::setACChargingPhaseCount(int phaseCount) void EverestEvse::initialize() { - qCDebug(dcEverest()) << "Evse: Initializing data for" << m_thing->name(); + qCDebug(dcEverest()) << "Evse: Starting to initialize the data for" << m_thing->name(); // Fetch all initial data for this device, once done we get notifications on data changes - EverestJsonRpcReply *reply = nullptr; - reply = m_client->evseGetInfo(m_index); - m_pendingInitReplies.append(reply); + EverestJsonRpcReply *reply = m_client->evseGetInfo(m_index); connect(reply, &EverestJsonRpcReply::finished, reply, &EverestJsonRpcReply::deleteLater); connect(reply, &EverestJsonRpcReply::finished, this, [this, reply](){ qCDebug(dcEverest()) << "Evse: Reply finished" << m_client->serverUrl().toString() << reply->method(); if (reply->error()) { qCWarning(dcEverest()) << "Evse: JsonRpc reply finished with error" << reply->method() << reply->method() << reply->error(); - // FIXME: check what we do if an init call failes. Do we stay disconnected and show an error or do we ignore it... - } else { - QVariantMap result = reply->response().value("result").toMap(); - EverestJsonRpcClient::ResponseError error = EverestJsonRpcClient::parseResponseError(result.value("error").toString()); - if (error) { - qCWarning(dcEverest()) << "Evse: Reply finished with an error" << reply->method() << error; - // FIXME: check what we do if an init call failes. Do we stay disconnected and show an error or do we ignore it... - } else { - m_evseInfo = EverestJsonRpcClient::parseEvseInfo(result.value("info").toMap()); - } + m_client->disconnectFromServer(); + return; + } + QVariantMap result = reply->response().value("result").toMap(); + EverestJsonRpcClient::ResponseError error = EverestJsonRpcClient::parseResponseError(result.value("error").toString()); + if (error) { + qCWarning(dcEverest()) << "Evse: Reply finished with an error" << reply->method() << error; + m_client->disconnectFromServer(); + return; } - // Check if we are done with the init process of this EVSE - evaluateInitFinished(reply); - }); + // Store data, thy will be processed once all replies arrived + m_evseInfo = EverestJsonRpcClient::parseEvseInfo(result.value("info").toMap()); - reply = m_client->evseGetHardwareCapabilities(m_index); - m_pendingInitReplies.append(reply); - connect(reply, &EverestJsonRpcReply::finished, reply, &EverestJsonRpcReply::deleteLater); - connect(reply, &EverestJsonRpcReply::finished, this, [this, reply](){ - qCDebug(dcEverest()) << "Evse: Reply finished" << m_client->serverUrl().toString() << reply->method(); - if (reply->error()) { - qCWarning(dcEverest()) << "Evse: JsonRpc reply finished with error" << reply->method() << reply->method() << reply->error(); - // FIXME: check what we do if an init call failes. Do we stay disconnected and show an error or do we ignore it... - } else { - QVariantMap result = reply->response().value("result").toMap(); - EverestJsonRpcClient::ResponseError error = EverestJsonRpcClient::parseResponseError(result.value("error").toString()); - if (error) { - qCWarning(dcEverest()) << "Evse: Reply finished with an error" << reply->method() << error; - // FIXME: check what we do if an init call failes. Do we stay disconnected and show an error or do we ignore it... - } else { - // Store data, thy will be processed once all replies arrived - m_hardwareCapabilities = EverestJsonRpcClient::parseHardwareCapabilities(result.value("hardware_capabilities").toMap()); + + EverestJsonRpcReply *reply = m_client->evseGetHardwareCapabilities(m_index); + connect(reply, &EverestJsonRpcReply::finished, reply, &EverestJsonRpcReply::deleteLater); + connect(reply, &EverestJsonRpcReply::finished, this, [this, reply](){ + qCDebug(dcEverest()) << "Evse: Reply finished" << m_client->serverUrl().toString() << reply->method(); + if (reply->error()) { + qCWarning(dcEverest()) << "Evse: JsonRpc reply finished with error" << reply->method() << reply->method() << reply->error(); + m_client->disconnectFromServer(); + return; } - } - - // Check if we are done with the init process of this EVSE - evaluateInitFinished(reply); - }); - - reply = m_client->evseGetStatus(m_index); - m_pendingInitReplies.append(reply); - connect(reply, &EverestJsonRpcReply::finished, reply, &EverestJsonRpcReply::deleteLater); - connect(reply, &EverestJsonRpcReply::finished, this, [this, reply](){ - qCDebug(dcEverest()) << "Evse: Reply finished" << m_client->serverUrl().toString() << reply->method(); - if (reply->error()) { - qCWarning(dcEverest()) << "Evse: JsonRpc reply finished with error" << reply->method() << reply->method() << reply->error(); - // FIXME: check what we do if an init call failes. Do we stay disconnected and show an error or do we ignore it... - } else { QVariantMap result = reply->response().value("result").toMap(); EverestJsonRpcClient::ResponseError error = EverestJsonRpcClient::parseResponseError(result.value("error").toString()); if (error) { qCWarning(dcEverest()) << "Evse: Reply finished with an error" << reply->method() << error; - // FIXME: check what we do if an init call failes. Do we stay disconnected and show an error or do we ignore it... - } else { + m_client->disconnectFromServer(); + return; + } + + // Store data, thy will be processed once all replies arrived + m_hardwareCapabilities = EverestJsonRpcClient::parseHardwareCapabilities(result.value("hardware_capabilities").toMap()); + + + EverestJsonRpcReply *reply = m_client->evseGetStatus(m_index); + connect(reply, &EverestJsonRpcReply::finished, reply, &EverestJsonRpcReply::deleteLater); + connect(reply, &EverestJsonRpcReply::finished, this, [this, reply](){ + qCDebug(dcEverest()) << "Evse: Reply finished" << m_client->serverUrl().toString() << reply->method(); + if (reply->error()) { + qCWarning(dcEverest()) << "Evse: JsonRpc reply finished with error" << reply->method() << reply->method() << reply->error(); + m_client->disconnectFromServer(); + return; + } + + QVariantMap result = reply->response().value("result").toMap(); + EverestJsonRpcClient::ResponseError error = EverestJsonRpcClient::parseResponseError(result.value("error").toString()); + if (error) { + qCWarning(dcEverest()) << "Evse: Reply finished with an error" << reply->method() << error; + m_client->disconnectFromServer(); + return; + } + // Store data, thy will be processed once all replies arrived m_evseStatus = EverestJsonRpcClient::parseEvseStatus(result.value("status").toMap()); - } - } - // Check if we are done with the init process of this EVSE - evaluateInitFinished(reply); + EverestJsonRpcReply *reply = m_client->evseGetMeterData(m_index); + connect(reply, &EverestJsonRpcReply::finished, reply, &EverestJsonRpcReply::deleteLater); + connect(reply, &EverestJsonRpcReply::finished, this, [this, reply](){ + qCDebug(dcEverest()) << "Evse: Reply finished" << m_client->serverUrl().toString() << reply->method(); + if (reply->error()) { + qCWarning(dcEverest()) << "Evse: JsonRpc reply finished with error" << reply->method() << reply->method() << reply->error(); + // FIXME: check what we do if an init call failes. Do we stay disconnected and show an error or do we ignore it... + } + QVariantMap result = reply->response().value("result").toMap(); + EverestJsonRpcClient::ResponseError error = EverestJsonRpcClient::parseResponseError(result.value("error").toString()); + if (error) { + if (error == EverestJsonRpcClient::ResponseErrorErrorNoDataAvailable) { + qCDebug(dcEverest()) << "Evse: There are no meter data available. Either there is no meter or the meter data are not available yet on EVSE side."; + } else { + // FIXME: check what we do if an init call failes. Do we stay disconnected and show an error or do we ignore it... + qCWarning(dcEverest()) << "Evse: Reply finished with an error" << reply->method() << error; + } + } + // Store data, thy will be processed once all replies arrived + m_meterData = EverestJsonRpcClient::parseMeterData(result.value("meter_data").toMap()); + + qCDebug(dcEverest()) << "Evse: The initialization of" << m_thing->name() << "has finished, the charger is now connected."; + m_initialized = true; + + // Set all initial states + m_thing->setStateValue("connected", true); + + // Process all data after beeing connected + processEvseStatus(); + processHardwareCapabilities(); + processMeterData(); + + }); + }); + }); }); - - reply = m_client->evseGetMeterData(m_index); - m_pendingInitReplies.append(reply); - connect(reply, &EverestJsonRpcReply::finished, reply, &EverestJsonRpcReply::deleteLater); - connect(reply, &EverestJsonRpcReply::finished, this, [this, reply](){ - qCDebug(dcEverest()) << "Evse: Reply finished" << m_client->serverUrl().toString() << reply->method(); - if (reply->error()) { - qCWarning(dcEverest()) << "Evse: JsonRpc reply finished with error" << reply->method() << reply->method() << reply->error(); - // FIXME: check what we do if an init call failes. Do we stay disconnected and show an error or do we ignore it... - } else { - QVariantMap result = reply->response().value("result").toMap(); - EverestJsonRpcClient::ResponseError error = EverestJsonRpcClient::parseResponseError(result.value("error").toString()); - if (error) { - if (error == EverestJsonRpcClient::ResponseErrorErrorNoDataAvailable) { - qCDebug(dcEverest()) << "Evse: There are no meter data available. Either there is no meter or the meter data are not available yet on EVSE side."; - } else { - // FIXME: check what we do if an init call failes. Do we stay disconnected and show an error or do we ignore it... - qCWarning(dcEverest()) << "Evse: Reply finished with an error" << reply->method() << error; - } - } else { - // Store data, thy will be processed once all replies arrived - m_meterData = EverestJsonRpcClient::parseMeterData(result.value("meter_data").toMap()); - } - } - - // Check if we are done with the init process of this EVSE - evaluateInitFinished(reply); - }); -} - -void EverestEvse::evaluateInitFinished(EverestJsonRpcReply *reply) -{ - if (m_initialized) - return; - - m_pendingInitReplies.removeAll(reply); - - if (m_pendingInitReplies.isEmpty()) { - qCDebug(dcEverest()) << "Evse: The initialization of" << m_thing->name() << "has finished, the charger is now connected."; - m_initialized = true; - - // Set all initial states - m_thing->setStateValue("connected", true); - - // Process all data after beeing conected - processEvseStatus(); - processHardwareCapabilities(); - processMeterData(); - } } void EverestEvse::processEvseStatus() @@ -243,7 +222,9 @@ void EverestEvse::processEvseStatus() m_thing->setStateValue(everestChargerAcPhaseCountStateTypeId, m_evseStatus.acChargeStatus.activePhaseCount); m_thing->setStateValue(everestChargerAcDesiredPhaseCountStateTypeId, m_evseStatus.acChargeStatus.activePhaseCount); - m_thing->setStateValue(everestChargerAcMaxChargingCurrentStateTypeId, m_evseStatus.acChargeParameters.maxCurrent); + if (m_evseStatus.acChargeParameters.maxCurrent > 0) + m_thing->setStateValue(everestChargerAcMaxChargingCurrentStateTypeId, m_evseStatus.acChargeParameters.maxCurrent); + } } diff --git a/everest/jsonrpc/everestevse.h b/everest/jsonrpc/everestevse.h index 299a303c..ef6d459d 100644 --- a/everest/jsonrpc/everestevse.h +++ b/everest/jsonrpc/everestevse.h @@ -59,10 +59,7 @@ private: EverestJsonRpcClient::HardwareCapabilities m_hardwareCapabilities; EverestJsonRpcClient::MeterData m_meterData; - QVector m_pendingInitReplies; - void initialize(); - void evaluateInitFinished(EverestJsonRpcReply *reply); void processEvseStatus(); void processHardwareCapabilities(); diff --git a/everest/jsonrpc/everestjsonrpcclient.cpp b/everest/jsonrpc/everestjsonrpcclient.cpp index 177391e5..5470595e 100644 --- a/everest/jsonrpc/everestjsonrpcclient.cpp +++ b/everest/jsonrpc/everestjsonrpcclient.cpp @@ -128,9 +128,10 @@ EverestJsonRpcClient::EverestJsonRpcClient(QObject *parent) m_available = false; emit availableChanged(m_available); } + + emit connectionErrorOccurred(); } }); - } QUrl EverestJsonRpcClient::serverUrl() @@ -168,7 +169,7 @@ EverestJsonRpcReply *EverestJsonRpcClient::evseGetInfo(int evseIndex) QVariantMap params; params.insert("evse_index", evseIndex); - EverestJsonRpcReply *reply = createReply("EVSE.GetInfo", params); + EverestJsonRpcReply *reply = createReply("EVSE.GetInfo", params, true); qCDebug(dcEverest()) << "Calling" << reply->method() << params; sendRequest(reply); return reply; @@ -179,7 +180,7 @@ EverestJsonRpcReply *EverestJsonRpcClient::evseGetStatus(int evseIndex) QVariantMap params; params.insert("evse_index", evseIndex); - EverestJsonRpcReply *reply = createReply("EVSE.GetStatus", params); + EverestJsonRpcReply *reply = createReply("EVSE.GetStatus", params, true); qCDebug(dcEverest()) << "Calling" << reply->method() << params; sendRequest(reply); return reply; @@ -190,7 +191,7 @@ EverestJsonRpcReply *EverestJsonRpcClient::evseGetHardwareCapabilities(int evseI QVariantMap params; params.insert("evse_index", evseIndex); - EverestJsonRpcReply *reply = createReply("EVSE.GetHardwareCapabilities", params); + EverestJsonRpcReply *reply = createReply("EVSE.GetHardwareCapabilities", params, true); qCDebug(dcEverest()) << "Calling" << reply->method() << params; sendRequest(reply); return reply; @@ -201,7 +202,8 @@ EverestJsonRpcReply *EverestJsonRpcClient::evseGetMeterData(int evseIndex) QVariantMap params; params.insert("evse_index", evseIndex); - EverestJsonRpcReply *reply = createReply("EVSE.GetMeterData", params); + // FIXME: do not retry... + EverestJsonRpcReply *reply = createReply("EVSE.GetMeterData", params, true); qCDebug(dcEverest()) << "Calling" << reply->method() << params; sendRequest(reply); return reply; @@ -461,6 +463,24 @@ void EverestJsonRpcClient::processDataPacket(const QByteArray &data) if (reply) { reply->setResponse(dataMap); + if (reply->retry()) { + QVariantMap result = reply->response().value("result").toMap(); + EverestJsonRpcClient::ResponseError error = EverestJsonRpcClient::parseResponseError(result.value("error").toString()); + if (error == EverestJsonRpcClient::ResponseErrorErrorNoDataAvailable) { + reply->m_retryCount++; + + if (reply->retryCount() <= reply->retryLimit()) { + qCDebug(dcEverest()) << "Reply for" << reply->method() << "has no data available yet. Retry" << reply->retryCount() << "/" << reply->retryLimit(); + reply->m_commandId = getNextCommandId(); + QTimer::singleShot(2000, this, [this, reply](){ sendRequest(reply); }); + // Retry scheduled, we are done with this packet + return; + } else { + qCWarning(dcEverest()) << "Reply for" << reply->method() << "has still no data available. Retry limit of" << reply->retryLimit() << "reached. Finish reply with error."; + } + } + } + // Verify if we received a json rpc error if (dataMap.contains("error")) { reply->finishReply(EverestJsonRpcReply::ErrorJsonRpcError); @@ -499,12 +519,16 @@ void EverestJsonRpcClient::processDataPacket(const QByteArray &data) } } -EverestJsonRpcReply *EverestJsonRpcClient::createReply(QString method, QVariantMap params) +EverestJsonRpcReply *EverestJsonRpcClient::createReply(QString method, QVariantMap params, bool retry) { - int commandId = m_commandId; - m_commandId += 1; + EverestJsonRpcReply *reply = new EverestJsonRpcReply(getNextCommandId(), method, params, this); + reply->m_retry = retry; + return reply; +} - return new EverestJsonRpcReply(commandId, method, params, this); +int EverestJsonRpcClient::getNextCommandId() +{ + return m_commandId++; } EverestJsonRpcReply *EverestJsonRpcClient::apiHello() diff --git a/everest/jsonrpc/everestjsonrpcclient.h b/everest/jsonrpc/everestjsonrpcclient.h index f7ff7dc2..51b4cc40 100644 --- a/everest/jsonrpc/everestjsonrpcclient.h +++ b/everest/jsonrpc/everestjsonrpcclient.h @@ -272,7 +272,7 @@ private: EverestJsonRpcInterface *m_interface = nullptr; QHash m_replies; - EverestJsonRpcReply *createReply(QString method, QVariantMap params = QVariantMap()); + EverestJsonRpcReply *createReply(QString method, QVariantMap params = QVariantMap(), bool retry = false); // Init infos QString m_apiVersion; @@ -281,6 +281,8 @@ private: bool m_authenticationRequired = false; QList m_evseInfos; + int getNextCommandId(); + // API calls EverestJsonRpcReply *apiHello(); EverestJsonRpcReply *chargePointGetEVSEInfos(); diff --git a/everest/jsonrpc/everestjsonrpcinterface.cpp b/everest/jsonrpc/everestjsonrpcinterface.cpp index 506ed001..a3d871c3 100644 --- a/everest/jsonrpc/everestjsonrpcinterface.cpp +++ b/everest/jsonrpc/everestjsonrpcinterface.cpp @@ -39,8 +39,13 @@ EverestJsonRpcInterface::EverestJsonRpcInterface(QObject *parent) connect(m_webSocket, &QWebSocket::disconnected, this, &EverestJsonRpcInterface::onDisconnected); connect(m_webSocket, &QWebSocket::textMessageReceived, this, &EverestJsonRpcInterface::onTextMessageReceived); connect(m_webSocket, &QWebSocket::binaryMessageReceived, this, &EverestJsonRpcInterface::onBinaryMessageReceived); + connect(m_webSocket, &QWebSocket::stateChanged, this, &EverestJsonRpcInterface::onStateChanged); +#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) + connect(m_webSocket, &QWebSocket::errorOccurred, this, &EverestJsonRpcInterface::onError); +#else connect(m_webSocket, SIGNAL(error(QAbstractSocket::SocketError)), this, SLOT(onError(QAbstractSocket::SocketError))); - connect(m_webSocket, SIGNAL(stateChanged(QAbstractSocket::SocketState)), this, SLOT(onStateChanged(QAbstractSocket::SocketState))); +#endif + } EverestJsonRpcInterface::~EverestJsonRpcInterface() @@ -91,6 +96,9 @@ void EverestJsonRpcInterface::onDisconnected() void EverestJsonRpcInterface::onError(QAbstractSocket::SocketError error) { qCDebug(dcEverest()) << "Socket error occurred" << error << m_webSocket->errorString(); + if (error == QAbstractSocket::ConnectionRefusedError) + emit connectedChanged(false); + } void EverestJsonRpcInterface::onStateChanged(QAbstractSocket::SocketState state) diff --git a/everest/jsonrpc/everestjsonrpcreply.cpp b/everest/jsonrpc/everestjsonrpcreply.cpp index 2042ddc5..7c5cbce7 100644 --- a/everest/jsonrpc/everestjsonrpcreply.cpp +++ b/everest/jsonrpc/everestjsonrpcreply.cpp @@ -76,6 +76,21 @@ QVariantMap EverestJsonRpcReply::requestMap() return request; } +bool EverestJsonRpcReply::retry() const +{ + return m_retry; +} + +int EverestJsonRpcReply::retryCount() const +{ + return m_retryCount; +} + +int EverestJsonRpcReply::retryLimit() const +{ + return m_retryLimit; +} + QVariantMap EverestJsonRpcReply::response() const { return m_response; diff --git a/everest/jsonrpc/everestjsonrpcreply.h b/everest/jsonrpc/everestjsonrpcreply.h index da702b56..d4c37973 100644 --- a/everest/jsonrpc/everestjsonrpcreply.h +++ b/everest/jsonrpc/everestjsonrpcreply.h @@ -58,6 +58,12 @@ public: QVariantMap params() const; QVariantMap requestMap(); + // Retry logic, as of now only for init requests and if + // they return ResponseErrorErrorNoDataAvailable + bool retry() const; + int retryCount() const; + int retryLimit() const; + // Response QVariantMap response() const; @@ -75,6 +81,10 @@ private: QTimer m_timer; Error m_error = ErrorNoError; + bool m_retry = false; + int m_retryCount = 0; + int m_retryLimit = 5; + void setResponse(const QVariantMap &response); void startWaiting(); void finishReply(Error error = ErrorNoError);