// SPDX-License-Identifier: GPL-3.0-or-later /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * Copyright (C) 2013 - 2024, nymea GmbH * Copyright (C) 2024 - 2025, chargebyte austria GmbH * * This file is part of nymea-plugins. * * nymea-plugins is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * nymea-plugins 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 * General Public License for more details. * * You should have received a copy of the GNU General Public License * along with nymea-plugins. If not, see . * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ #include "everestjsonrpcclient.h" #include "extern-plugininfo.h" #include #include #include Q_DECLARE_LOGGING_CATEGORY(dcEverestTraffic) EverestJsonRpcClient::EverestJsonRpcClient(QObject *parent) : QObject{parent}, m_interface{new EverestJsonRpcInterface(this)} { connect(m_interface, &EverestJsonRpcInterface::dataReceived, this, &EverestJsonRpcClient::processDataPacket); connect(m_interface, &EverestJsonRpcInterface::connectedChanged, this, [this](bool connected){ qCDebug(dcEverest()) << "Interface is" << (connected ? "now connected" : "not connected any more"); if (connected) { // The interface is connected, fetch initial data and mark the client as available if successfull, // otherwise emit connection error signal and close the connection EverestJsonRpcReply *reply = apiHello(); connect(reply, &EverestJsonRpcReply::finished, reply, &EverestJsonRpcReply::deleteLater); connect(reply, &EverestJsonRpcReply::finished, this, [this, reply](){ qCDebug(dcEverest()) << "Reply finished" << m_interface->serverUrl().toString() << reply->method(); if (reply->error()) { qCWarning(dcEverest()) << "JsonRpc reply finished with error" << reply->method() << reply->error(); disconnectFromServer(); emit connectionErrorOccurred(); return; } // Verify data format and API version QVariantMap result = reply->response().value("result").toMap(); if (!result.contains("api_version") || !result.contains("everest_version") || !result.contains("charger_info")) { qCWarning(dcEverest()) << "Missing expected properties in JsonRpc response" << reply->method(); disconnectFromServer(); emit connectionErrorOccurred(); return; } m_apiVersion = result.value("api_version").toString(); m_everestVersion = result.value("everest_version").toString(); m_authenticationRequired = result.value("authentication_required").toBool(); QVariantMap chargerInfoMap = result.value("charger_info").toMap(); m_chargerInfo.vendor = chargerInfoMap.value("vendor").toString(); m_chargerInfo.model = chargerInfoMap.value("model").toString(); m_chargerInfo.serialNumber = chargerInfoMap.value("serial").toString(); m_chargerInfo.firmwareVersion = chargerInfoMap.value("firmware_version").toString(); EverestJsonRpcReply *reply = chargePointGetEVSEInfos(); connect(reply, &EverestJsonRpcReply::finished, reply, &EverestJsonRpcReply::deleteLater); connect(reply, &EverestJsonRpcReply::finished, this, [this, reply](){ qCDebug(dcEverest()) << "Reply finished" << m_interface->serverUrl().toString() << reply->method(); if (reply->error()) { qCWarning(dcEverest()) << "JsonRpc reply finished with error" << reply->method() << reply->error(); disconnectFromServer(); emit connectionErrorOccurred(); return; } QVariantMap result = reply->response().value("result").toMap(); QString errorString = result.value("error").toString(); if (errorString != "NoError") { qCWarning(dcEverest()) << "Error requesting" << reply->method() << errorString; disconnectFromServer(); emit connectionErrorOccurred(); return; } m_evseInfos.clear(); foreach (const QVariant &evseInfoVariant, result.value("infos").toList()) { m_evseInfos.append(parseEvseInfo(evseInfoVariant.toMap())); } // We are done with the init and the client is now available if (!m_available) { m_available = true; emit availableChanged(m_available); } }); }); } else { // Client disconnected. Clean up any pending replies qCDebug(dcEverest()) << "Lost connection to the server. Finish any pending replies ..."; foreach (EverestJsonRpcReply *reply, m_replies) { reply->finishReply(EverestJsonRpcReply::ErrorConnectionError); } if (m_available) { m_available = false; emit availableChanged(m_available); } emit connectionErrorOccurred(); } }); } QUrl EverestJsonRpcClient::serverUrl() { return m_interface->serverUrl(); } bool EverestJsonRpcClient::available() const { return m_available; } QString EverestJsonRpcClient::apiVersion() const { return m_apiVersion; } QString EverestJsonRpcClient::everestVersion() const { return m_everestVersion; } QList EverestJsonRpcClient::evseInfos() const { return m_evseInfos; } EverestJsonRpcClient::ChargerInfo EverestJsonRpcClient::chargerInfo() const { return m_chargerInfo; } EverestJsonRpcReply *EverestJsonRpcClient::evseGetInfo(int evseIndex) { QVariantMap params; params.insert("evse_index", evseIndex); EverestJsonRpcReply *reply = createReply("EVSE.GetInfo", params, true); qCDebug(dcEverest()) << "Calling" << reply->method() << params; sendRequest(reply); return reply; } EverestJsonRpcReply *EverestJsonRpcClient::evseGetStatus(int evseIndex) { QVariantMap params; params.insert("evse_index", evseIndex); EverestJsonRpcReply *reply = createReply("EVSE.GetStatus", params, true); qCDebug(dcEverest()) << "Calling" << reply->method() << params; sendRequest(reply); return reply; } EverestJsonRpcReply *EverestJsonRpcClient::evseGetHardwareCapabilities(int evseIndex) { QVariantMap params; params.insert("evse_index", evseIndex); EverestJsonRpcReply *reply = createReply("EVSE.GetHardwareCapabilities", params, true); qCDebug(dcEverest()) << "Calling" << reply->method() << params; sendRequest(reply); return reply; } EverestJsonRpcReply *EverestJsonRpcClient::evseGetMeterData(int evseIndex) { QVariantMap params; params.insert("evse_index", evseIndex); // FIXME: do not retry... EverestJsonRpcReply *reply = createReply("EVSE.GetMeterData", params, true); qCDebug(dcEverest()) << "Calling" << reply->method() << params; sendRequest(reply); return reply; } EverestJsonRpcReply *EverestJsonRpcClient::evseSetChargingAllowed(int evseIndex, bool allowed) { QVariantMap params; params.insert("evse_index", evseIndex); params.insert("charging_allowed", allowed); EverestJsonRpcReply *reply = createReply("EVSE.SetChargingAllowed", params); qCDebug(dcEverest()) << "Calling" << reply->method() << params; sendRequest(reply); return reply; } EverestJsonRpcReply *EverestJsonRpcClient::evseSetACChargingCurrent(int evseIndex, double current) { QVariantMap params; params.insert("evse_index", evseIndex); params.insert("max_current", current); EverestJsonRpcReply *reply = createReply("EVSE.SetACChargingCurrent", params); qCDebug(dcEverest()) << "Calling" << reply->method() << params; sendRequest(reply); return reply; } EverestJsonRpcReply *EverestJsonRpcClient::evseSetACChargingPhaseCount(int evseIndex, int phaseCount) { QVariantMap params; params.insert("evse_index", evseIndex); params.insert("phase_count", phaseCount); EverestJsonRpcReply *reply = createReply("EVSE.SetACChargingPhaseCount", params); qCDebug(dcEverest()) << "Calling" << reply->method() << params; sendRequest(reply); return reply; } EverestJsonRpcClient::ResponseError EverestJsonRpcClient::parseResponseError(const QString &responseErrorString) { QMetaEnum metaEnum = QMetaEnum::fromType(); return static_cast(metaEnum.keyToValue(QString("ResponseError").append(responseErrorString).toUtf8())); } EverestJsonRpcClient::ConnectorType EverestJsonRpcClient::parseConnectorType(const QString &connectorTypeString) { QMetaEnum metaEnum = QMetaEnum::fromType(); return static_cast(metaEnum.keyToValue(QString("ConnectorType").append(connectorTypeString).toUtf8())); } EverestJsonRpcClient::ChargeProtocol EverestJsonRpcClient::parseChargeProtocol(const QString &chargeProtocolString) { QMetaEnum metaEnum = QMetaEnum::fromType(); return static_cast(metaEnum.keyToValue(QString("ChargeProtocol").append(chargeProtocolString).toUtf8())); } EverestJsonRpcClient::EvseState EverestJsonRpcClient::parseEvseState(const QString &evseStateString) { QMetaEnum metaEnum = QMetaEnum::fromType(); return static_cast(metaEnum.keyToValue(QString("EvseState").append(evseStateString).toUtf8())); } EverestJsonRpcClient::EVSEInfo EverestJsonRpcClient::parseEvseInfo(const QVariantMap &evseInfoMap) { EVSEInfo evseInfo; evseInfo.index = evseInfoMap.value("index").toInt(); evseInfo.id = evseInfoMap.value("id").toString(); evseInfo.bidirectionalCharging = evseInfoMap.value("bidi_charging").toBool(); foreach (const QVariant &connectorInfoVariant, evseInfoMap.value("available_connectors").toList()) { evseInfo.availableConnectors.append(parseConnectorInfo(connectorInfoVariant.toMap())); } return evseInfo; } EverestJsonRpcClient::ConnectorInfo EverestJsonRpcClient::parseConnectorInfo(const QVariantMap &connectorInfoMap) { ConnectorInfo connectorInfo; connectorInfo.connectorId = connectorInfoMap.value("id").toInt(); connectorInfo.type = parseConnectorType(connectorInfoMap.value("type").toString()); // optional if (connectorInfoMap.contains("description")) connectorInfo.description = connectorInfoMap.value("description").toString(); return connectorInfo; } EverestJsonRpcClient::EVSEStatus EverestJsonRpcClient::parseEvseStatus(const QVariantMap &evseStatusMap) { EVSEStatus evseStatus; evseStatus.chargedEnergyWh = evseStatusMap.value("charged_energy_wh").toDouble(); evseStatus.dischargedEnergyWh = evseStatusMap.value("discharged_energy_wh").toDouble(); evseStatus.chargingDuration = evseStatusMap.value("charging_duration_s").toInt(); evseStatus.chargingAllowed = evseStatusMap.value("charging_allowed").toBool(); evseStatus.available = evseStatusMap.value("available").toBool(); evseStatus.activeConnectorIndex = evseStatusMap.value("active_connector_index").toInt(); evseStatus.errorPresent = evseStatusMap.value("error_present").toBool(); evseStatus.chargeProtocol = parseChargeProtocol(evseStatusMap.value("charge_protocol").toString()); evseStatus.evseState = parseEvseState(evseStatusMap.value("state").toString()); evseStatus.evseStateString = evseStatusMap.value("state").toString(); // optional if (evseStatusMap.contains("ac_charge_status")) evseStatus.acChargeStatus = EverestJsonRpcClient::parseACChargeStatus(evseStatusMap.value("ac_charge_status").toMap()); // optional if (evseStatusMap.contains("ac_charge_param")) evseStatus.acChargeParameters = EverestJsonRpcClient::parseACChargeParameters(evseStatusMap.value("ac_charge_param").toMap()); return evseStatus; } EverestJsonRpcClient::ACChargeStatus EverestJsonRpcClient::parseACChargeStatus(const QVariantMap &acChargeStatusMap) { EverestJsonRpcClient::ACChargeStatus status; status.activePhaseCount = acChargeStatusMap.value("evse_active_phase_count").toInt(); return status; } EverestJsonRpcClient::ACChargeParameters EverestJsonRpcClient::parseACChargeParameters(const QVariantMap &acChargeParametersMap) { EverestJsonRpcClient::ACChargeParameters params; params.maxCurrent = acChargeParametersMap.value("evse_max_current").toInt(); params.maxPhaseCount = acChargeParametersMap.value("evse_max_phase_count").toInt(); return params; } EverestJsonRpcClient::HardwareCapabilities EverestJsonRpcClient::parseHardwareCapabilities(const QVariantMap &hardwareCapabilitiesMap) { HardwareCapabilities hardwareCapabilities; hardwareCapabilities.maxCurrentExport = hardwareCapabilitiesMap.value("max_current_A_export").toDouble(); hardwareCapabilities.maxCurrentImport = hardwareCapabilitiesMap.value("max_current_A_import").toDouble(); hardwareCapabilities.maxPhaseCountExport = hardwareCapabilitiesMap.value("max_phase_count_export").toInt(); hardwareCapabilities.maxPhaseCountImport = hardwareCapabilitiesMap.value("max_phase_count_import").toInt(); hardwareCapabilities.minCurrentExport = hardwareCapabilitiesMap.value("min_current_A_export").toDouble(); hardwareCapabilities.minCurrentImport = hardwareCapabilitiesMap.value("min_current_A_import").toDouble(); hardwareCapabilities.minPhaseCountExport = hardwareCapabilitiesMap.value("min_phase_count_export").toInt(); hardwareCapabilities.minPhaseCountImport = hardwareCapabilitiesMap.value("min_phase_count_import").toInt(); hardwareCapabilities.phaseSwitchDuringCharging = hardwareCapabilitiesMap.value("phase_switch_during_charging").toBool(); return hardwareCapabilities; } EverestJsonRpcClient::MeterData EverestJsonRpcClient::parseMeterData(const QVariantMap &meterDataMap) { MeterData meterData; meterData.meterId = meterDataMap.value("meter_id").toString(); meterData.energyImportedL1 = meterDataMap.value("energy_Wh_import").toMap().value("L1").toFloat(); meterData.energyImportedL2 = meterDataMap.value("energy_Wh_import").toMap().value("L2").toFloat(); meterData.energyImportedL3 = meterDataMap.value("energy_Wh_import").toMap().value("L3").toFloat(); meterData.energyImportedTotal = meterDataMap.value("energy_Wh_import").toMap().value("total").toFloat(); // optional if (meterDataMap.contains("serial_number")) meterData.serialNumber = meterDataMap.value("serial_number").toString(); // optional if (meterDataMap.contains("phase_seq_error")) meterData.phaseSequenceError = meterDataMap.value("phase_seq_error").toBool(); // optional if (meterDataMap.contains("power_W")) { meterData.powerL1 = meterDataMap.value("power_W").toMap().value("L1").toFloat(); meterData.powerL2 = meterDataMap.value("power_W").toMap().value("L2").toFloat(); meterData.powerL3 = meterDataMap.value("power_W").toMap().value("L3").toFloat(); meterData.powerTotal = meterDataMap.value("power_W").toMap().value("total").toFloat(); } // optional if (meterDataMap.contains("voltage_V")) { meterData.voltageL1 = meterDataMap.value("voltage_V").toMap().value("L1").toFloat(); meterData.voltageL2 = meterDataMap.value("voltage_V").toMap().value("L2").toFloat(); meterData.voltageL3 = meterDataMap.value("voltage_V").toMap().value("L3").toFloat(); } // optional if (meterDataMap.contains("current_A")) { meterData.currentL1 = meterDataMap.value("current_A").toMap().value("L1").toFloat(); meterData.currentL2 = meterDataMap.value("current_A").toMap().value("L2").toFloat(); meterData.currentL3 = meterDataMap.value("current_A").toMap().value("L3").toFloat(); meterData.currentN = meterDataMap.value("current_A").toMap().value("N").toFloat(); } // optional if (meterDataMap.contains("energy_Wh_export")) { meterData.energyExportedL1 = meterDataMap.value("energy_Wh_export").toMap().value("L1").toFloat(); meterData.energyExportedL2 = meterDataMap.value("energy_Wh_export").toMap().value("L2").toFloat(); meterData.energyExportedL3 = meterDataMap.value("energy_Wh_export").toMap().value("L3").toFloat(); meterData.energyExportedTotal = meterDataMap.value("energy_Wh_export").toMap().value("total").toFloat(); } // optional if (meterDataMap.contains("frequency_Hz")) { meterData.frequencyL1 = meterDataMap.value("frequency_Hz").toMap().value("L1").toFloat(); meterData.frequencyL2 = meterDataMap.value("frequency_Hz").toMap().value("L2").toFloat(); meterData.frequencyL3 = meterDataMap.value("frequency_Hz").toMap().value("L3").toFloat(); } return meterData; } void EverestJsonRpcClient::connectToServer(const QUrl &serverUrl) { m_interface->connectServer(serverUrl); } void EverestJsonRpcClient::disconnectFromServer() { m_interface->disconnectServer(); } void EverestJsonRpcClient::sendRequest(EverestJsonRpcReply *reply) { QVariantMap requestMap = reply->requestMap(); QByteArray data = QJsonDocument::fromVariant(requestMap).toJson(QJsonDocument::Compact) + '\n'; m_replies.insert(reply->commandId(), reply); connect(reply, &EverestJsonRpcReply::finished, this, [this, reply](){ // Clean up internals after finished (in any error case), // it is up to the caller to delete the reply object itself m_replies.remove(reply->commandId()); }); qCDebug(dcEverestTraffic()) << "-->" << m_interface->serverUrl().toString() << qUtf8Printable(data); m_interface->sendData(data); reply->startWaiting(); } void EverestJsonRpcClient::processDataPacket(const QByteArray &data) { qCDebug(dcEverestTraffic()) << "<--" << m_interface->serverUrl().toString() << qUtf8Printable(data); QJsonParseError error; QJsonDocument jsonDoc = QJsonDocument::fromJson(data, &error); if (error.error != QJsonParseError::NoError) { qCWarning(dcEverest()) << "Invalid JSON data recived" << m_interface->serverUrl().toString() << error.errorString(); return; } QVariantMap dataMap = jsonDoc.toVariant().toMap(); if (dataMap.value("jsonrpc").toString() != "2.0") { qCWarning(dcEverest()) << "Received valid JSON data but does not seem to be a JSON RPC 2.0 format" << m_interface->serverUrl().toString() << qUtf8Printable(data); return; } if (dataMap.contains("id")) { // Response to a request int commandId = dataMap.value("id").toInt(); EverestJsonRpcReply *reply = m_replies.take(commandId); 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); } else { reply->finishReply(); } return; } else { // Data without reply, check if this is a notification qCDebug(dcEverest()) << "Received response data without any pending reply, maybe the reply timed out:" << qUtf8Printable(data); } } else { // A Notification is a Request object without an "id" member. QString notification = dataMap.value("method").toString(); QVariantMap params = dataMap.value("params").toMap(); qCDebug(dcEverest()) << "Received notification" << notification; if (notification == "EVSE.StatusChanged") { int evseIndex = params.value("evse_index").toInt(); EVSEStatus evseStatus = EverestJsonRpcClient::parseEvseStatus(params.value("evse_status").toMap()); emit evseStatusChanged(evseIndex, evseStatus); } else if (notification == "ChargePoint.ActiveErrorsChanged") { // TODO qCWarning(dcEverest()) << "Active errors changed" << qUtf8Printable(QJsonDocument::fromVariant(params).toJson()); } else if (notification == "EVSE.HardwareCapabilitiesChanged") { int evseIndex = params.value("evse_index").toInt(); HardwareCapabilities hardwareCapabilities = EverestJsonRpcClient::parseHardwareCapabilities(params.value("hardware_capabilities").toMap()); emit hardwareCapabilitiesChanged(evseIndex, hardwareCapabilities); } else if (notification == "EVSE.MeterDataChanged") { int evseIndex = params.value("evse_index").toInt(); MeterData meterData = EverestJsonRpcClient::parseMeterData(params.value("meter_data").toMap()); emit meterDataChanged(evseIndex, meterData); } } } EverestJsonRpcReply *EverestJsonRpcClient::createReply(QString method, QVariantMap params, bool retry) { EverestJsonRpcReply *reply = new EverestJsonRpcReply(getNextCommandId(), method, params, this); reply->m_retry = retry; return reply; } int EverestJsonRpcClient::getNextCommandId() { return m_commandId++; } EverestJsonRpcReply *EverestJsonRpcClient::apiHello() { EverestJsonRpcReply *reply = createReply("API.Hello", QVariantMap()); qCDebug(dcEverest()) << "Calling" << reply->method(); sendRequest(reply); return reply; } EverestJsonRpcReply *EverestJsonRpcClient::chargePointGetEVSEInfos() { EverestJsonRpcReply *reply = createReply("ChargePoint.GetEVSEInfos", QVariantMap()); qCDebug(dcEverest()) << "Calling" << reply->method(); sendRequest(reply); return reply; }