/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * Copyright 2013 - 2021, nymea GmbH * Contact: contact@nymea.io * * This file is part of nymea. * This project including source code and documentation is protected by * copyright law, and remains the property of nymea GmbH. All rights, including * reproduction, publication, editing and translation, are reserved. The use of * this project is subject to the terms of a license agreement to be concluded * with nymea GmbH in accordance with the terms of use of nymea GmbH, available * under https://nymea.io/license * * GNU Lesser General Public License Usage * Alternatively, this project may be redistributed and/or modified under the * terms of the GNU Lesser General Public License as published by the Free * Software Foundation; version 3. This project is distributed in the hope that * it will be useful, but WITHOUT ANY WARRANTY; without even the implied * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this project. If not, see . * * For any further details and any questions please contact us under * contact@nymea.io or see our FAQ/Licensing Information on * https://nymea.io/license/faq * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ #include "kecontact.h" #include "extern-plugininfo.h" #include KeContact::KeContact(const QHostAddress &address, KeContactDataLayer *dataLayer, QObject *parent) : QObject(parent), m_dataLayer(dataLayer), m_address(address) { qCDebug(dcKeba()) << "Creating KeContact connection for address" << m_address; m_requestTimeoutTimer = new QTimer(this); m_requestTimeoutTimer->setSingleShot(true); connect(m_requestTimeoutTimer, &QTimer::timeout, this, [this]() { // This timer will be started when a request is sent and stopped or resetted when a response has been received setReachable(false); if (m_currentRequest.isValid()) { // Schedule pause timer to send next request qCWarning(dcKeba()) << "Command timeouted" << m_currentRequest.command(); emit commandExecuted(m_currentRequest.requestId(), false); } // Timeout...send the next request right the way since at least 5 seconds passed since tha last command m_currentRequest = KeContactRequest(); sendNextCommand(); }); m_pauseTimer = new QTimer(this); m_pauseTimer->setSingleShot(true); connect(m_pauseTimer, &QTimer::timeout, this, [this](){ sendNextCommand(); }); connect(m_dataLayer, &KeContactDataLayer::datagramReceived, this, &KeContact::onReceivedDatagram); } KeContact::~KeContact() { qCDebug(dcKeba()) << "Deleting KeContact connection for address" << m_address.toString(); } QHostAddress KeContact::address() const { return m_address; } QUuid KeContact::start(const QByteArray &rfidToken, const QByteArray &rfidClassifier) { if (!m_dataLayer) { qCWarning(dcKeba()) << "UDP socket not initialized"; setReachable(false); return QUuid(); } QByteArray datagram = "start " + rfidToken + " " + rfidClassifier; KeContactRequest request(QUuid::createUuid(), datagram); qCDebug(dcKeba()) << "Start: Datagram:" << datagram; m_requestQueue.enqueue(request); sendNextCommand(); return request.requestId(); } QUuid KeContact::stop(const QByteArray &rfidToken) { if (!m_dataLayer) { qCWarning(dcKeba()) << "UDP socket not initialized"; setReachable(false); return QUuid(); } QByteArray datagram = "stop " + rfidToken; KeContactRequest request(QUuid::createUuid(), datagram); qCDebug(dcKeba()) << "Stop: Datagram:" << datagram; m_requestQueue.enqueue(request); sendNextCommand(); return request.requestId(); } void KeContact::setAddress(const QHostAddress &address) { if (m_address == address) return; qCDebug(dcKeba()) << "Updating Keba connection address from" << m_address.toString() << "to" << address.toString(); m_address = address; } bool KeContact::reachable() const { return m_reachable; } void KeContact::sendCommand(const QByteArray &command) { if (!m_dataLayer) { qCWarning(dcKeba()) << "UDP socket not initialized"; setReachable(false); return; } m_dataLayer->write(m_address, command); m_requestTimeoutTimer->start(5000); } void KeContact::sendNextCommand() { // No message left, we are done if (m_requestQueue.isEmpty()) return; // Still a request pending if (m_currentRequest.isValid()) return; m_currentRequest = m_requestQueue.dequeue(); sendCommand(m_currentRequest.command()); } void KeContact::setReachable(bool reachable) { if (m_reachable == reachable) return; if (reachable) { qCDebug(dcKeba()) << "The keba wallbox on" << m_address.toString() << "is now reachable again."; } else { qCWarning(dcKeba()) << "The keba wallbox on" << m_address.toString() << "is not reachable any more."; m_requestQueue.clear(); m_currentRequest = KeContactRequest(); } m_reachable = reachable; emit reachableChanged(m_reachable); } QUuid KeContact::enableOutput(bool state) { if (!m_dataLayer) { qCWarning(dcKeba()) << "UDP socket not initialized"; setReachable(false); return QUuid(); } // Print information that we are executing now the update action; QByteArray datagram; if (state){ datagram.append("ena 1"); } else{ datagram.append("ena 0"); } KeContactRequest request(QUuid::createUuid(), datagram); request.setDelayUntilNextCommand(2000); qCDebug(dcKeba()) << "Enable output: Datagram:" << datagram; m_requestQueue.enqueue(request); sendNextCommand(); return request.requestId(); } QUuid KeContact::setMaxAmpere(int milliAmpere) { if (!m_dataLayer) { qCWarning(dcKeba()) << "UDP socket not initialized"; setReachable(false); return QUuid(); } if (milliAmpere < 6000 || milliAmpere > 63000) { qCWarning(dcKeba()) << "KeContact: Set max ampere, currtime mA out of range [6000, 63000]" << milliAmpere; return QUuid(); } // Print information that we are executing now the update action qCDebug(dcKeba()) << "Update max current to : " << milliAmpere; QString commandLine = QString("currtime %1 1").arg(milliAmpere); QByteArray datagram = commandLine.toUtf8(); KeContactRequest request(QUuid::createUuid(), datagram); request.setDelayUntilNextCommand(1200); qCDebug(dcKeba()) << "Set max charging amps: Datagram:" << datagram; m_requestQueue.enqueue(request); sendNextCommand(); return request.requestId(); } QUuid KeContact::setMaxAmpereGeneral(int milliAmpere) { if (!m_dataLayer) { qCWarning(dcKeba()) << "UDP socket not initialized"; setReachable(false); return QUuid(); } if (milliAmpere < 6000 || milliAmpere > 63000) { qCWarning(dcKeba()) << "KeContact: Set max ampere curr, mA out of range [6000, 63000]" << milliAmpere; return QUuid(); } // Print information that we are executing now the update action qCDebug(dcKeba()) << "Update general max current to: " << milliAmpere; QString commandLine = QString("curr %1").arg(milliAmpere); QByteArray datagram = commandLine.toUtf8(); KeContactRequest request(QUuid::createUuid(), datagram); request.setDelayUntilNextCommand(1200); m_requestQueue.enqueue(request); sendNextCommand(); return request.requestId(); } QUuid KeContact::displayMessage(const QByteArray &message) { if (!m_dataLayer) { qCWarning(dcKeba()) << "UDP socket not initialized"; setReachable(false); return QUuid(); } /* Text shown on the display. Maximum 23 ASCII characters can be used. 0 .. 23 characters ~ == Σ $ == blank , == comma */ qCDebug(dcKeba()) << "Set display message: " << message; QByteArray datagram; QByteArray modifiedMessage = message; modifiedMessage.replace(" ", "$"); if (modifiedMessage.size() > 23) { modifiedMessage.resize(23); } datagram.append("display 0 0 0 0 " + modifiedMessage); KeContactRequest request(QUuid::createUuid(), datagram); qCDebug(dcKeba()) << "Display message: Datagram:" << datagram; m_requestQueue.enqueue(request); sendNextCommand(); return request.requestId(); } QUuid KeContact::chargeWithEnergyLimit(double energy) { if (!m_dataLayer) { qCWarning(dcKeba()) << "UDP socket not initialized"; setReachable(false); return QUuid(); } QByteArray datagram; datagram.append("setenergy " + QVariant(static_cast(energy*10000)).toByteArray()); KeContactRequest request(QUuid::createUuid(), datagram); qCDebug(dcKeba()) << "Charge with energy limit: Datagram: " << datagram; m_requestQueue.enqueue(request); sendNextCommand(); return request.requestId(); } QUuid KeContact::setFailsafe(int timeout, int current, bool save) { if (!m_dataLayer) { qCWarning(dcKeba()) << "UDP socket not initialized"; setReachable(false); return QUuid(); } QByteArray data; data.append("failsave"); data.append(" "+QVariant(timeout).toByteArray()); data.append(" "+QVariant(current).toByteArray()); data.append((save ? " 1":" 0")); KeContactRequest request(QUuid::createUuid(), data); qCDebug(dcKeba()) << "Set failsafe mode: Datagram: " << data; m_requestQueue.enqueue(request); sendNextCommand(); return request.requestId(); } void KeContact::getDeviceInformation() { QByteArray data; data.append("i"); KeContactRequest request(QUuid::createUuid(), data); qCDebug(dcKeba()) << "Get device information: Datagram: " << data; m_requestQueue.enqueue(request); sendNextCommand(); } void KeContact::getReport1() { getReport(1); } void KeContact::getReport2() { getReport(2); } void KeContact::getReport3() { getReport(3); } void KeContact::getReport1XX(int reportNumber) { getReport(reportNumber); } QUuid KeContact::setOutputX2(bool state) { if (!m_dataLayer) { qCWarning(dcKeba()) << "UDP socket not initialized"; setReachable(false); return QUuid(); } QByteArray datagram; datagram.append("output " + QVariant((state ? 1 : 0)).toByteArray()); KeContactRequest request(QUuid::createUuid(), datagram); qCDebug(dcKeba()) << "Set Output X2, state:" << state << "Datagram:" << datagram; m_requestQueue.enqueue(request); sendNextCommand(); return request.requestId(); } void KeContact::getReport(int reportNumber) { if (!m_dataLayer) { qCWarning(dcKeba()) << "UDP socket not initialized"; setReachable(false); return; } QByteArray datagram; datagram.append("report " + QVariant(reportNumber).toByteArray()); KeContactRequest request(QUuid::createUuid(), datagram); qCDebug(dcKeba()) << "Get report" << reportNumber << "Datagram:" << datagram; m_requestQueue.enqueue(request); sendNextCommand(); } QUuid KeContact::unlockCharger() { if (!m_dataLayer) { qCWarning(dcKeba()) << "UDP socket not initialized"; setReachable(false); return QUuid(); } QByteArray datagram; datagram.append("unlock"); KeContactRequest request(QUuid::createUuid(), datagram); qCDebug(dcKeba()) << "Unlock charger: Datagram:" << datagram; m_requestQueue.enqueue(request); sendNextCommand(); return request.requestId(); } void KeContact::onReceivedDatagram(const QHostAddress &address, const QByteArray &datagram) { // Make sure the datagram is for this keba if (address != m_address) return; if (datagram.contains("TCH-OK")){ // We received valid data from the address over the data link, so the wallbox must be reachable setReachable(true); //Command response has been received, now send the next command m_requestTimeoutTimer->stop(); if (m_currentRequest.isValid()) { if (datagram.contains("done")) { qCDebug(dcKeba()) << "Command" << m_currentRequest.command() << "finished successfully"; emit commandExecuted(m_currentRequest.requestId(), true); } else { qCWarning(dcKeba()) << "Command" << m_currentRequest.command() << "finished with error" << datagram; emit commandExecuted(m_currentRequest.requestId(), false); } // Schedule pause timer to send next request m_pauseTimer->start(m_currentRequest.delayUntilNextCommand()); m_currentRequest = KeContactRequest(); } else { //Probably the response has taken too long and the requestId has been already removed qCWarning(dcKeba()) << "Received command OK response without pending request." << datagram; } } else if (datagram.left(8).contains("Firmware")){ // We received valid data from the address over the data link, so the wallbox must be reachable setReachable(true); // Command response has been received, now send the next command m_requestTimeoutTimer->stop(); if (m_currentRequest.isValid()) { // Schedule pause timer to send next request m_pauseTimer->start(m_currentRequest.delayUntilNextCommand()); m_currentRequest = KeContactRequest(); } qCDebug(dcKeba()) << "Firmware information received"; QByteArrayList firmware = datagram.split(':'); if (firmware.length() >= 2) { emit deviceInformationReceived(firmware[1]); } } else { //Command response has been received, now send the next command m_requestTimeoutTimer->stop(); if (m_currentRequest.isValid()) { // Schedule pause timer to send next request m_pauseTimer->start(m_currentRequest.delayUntilNextCommand()); m_currentRequest = KeContactRequest(); } // Convert the rawdata to a json document QJsonParseError error; QJsonDocument jsonDoc = QJsonDocument::fromJson(datagram, &error); if (error.error != QJsonParseError::NoError) { qCWarning(dcKeba()) << "Failed to parse JSON data" << datagram << ":" << error.errorString(); return; } QVariantMap data = jsonDoc.toVariant().toMap(); if (data.contains("ID")) { int id = data.value("ID").toInt(); if (id == 1) { // We received valid data from the address over the data link, so the wallbox must be reachable setReachable(true); ReportOne reportOne; //qCDebug(dcKeba()) << "Report 1 received"; reportOne.product = data.value("Product").toString(); reportOne.firmware = data.value("Firmware").toString(); reportOne.serialNumber = data.value("Serial").toString(); //"Backend:" //"timeQ": 3 //"DIP-Sw1": "0x22" //"DIP-Sw2": reportOne.dipSw1 = data.value("DIP-Sw1").toString().remove("0x").toUInt(nullptr, 16); reportOne.dipSw2 = data.value("DIP-Sw2").toString().remove("0x").toUInt(nullptr, 16); if (data.contains("COM-module")) { reportOne.comModule = (data.value("COM-module").toInt() == 1); } else { reportOne.comModule = false; } if (data.contains("Sec")) { reportOne.comModule = data.value("Sec").toInt(); } else { reportOne.comModule = 0; } emit reportOneReceived(reportOne); } else if (id == 2) { // We received valid data from the address over the data link, so the wallbox must be reachable setReachable(true); ReportTwo reportTwo; //qCDebug(dcKeba()) << "Report 2 received"; int state = data.value("State").toInt(); reportTwo.state = State(state); reportTwo.error1 = data.value("Error1").toInt(); reportTwo.error2 = data.value("Error2").toInt(); reportTwo.plugState = PlugState(data.value("Plug").toInt()); reportTwo.enableUser = data.value("Enable user").toBool(); reportTwo.enableSys = data.value("Enable sys").toBool(); reportTwo.maxCurrent = data.value("Max curr").toInt() / 1000.00; reportTwo.maxCurrentPercentage = data.value("Max curr %").toInt() / 10.00; reportTwo.currentHardwareLimitation = data.value("Curr HW").toInt() / 1000.00; reportTwo.currentUser = data.value("Curr user").toInt() / 1000.00; reportTwo.currTimer = data.value("Curr timer").toInt() / 1000.00; reportTwo.timeoutCt = data.value("Tmo CT").toInt(); reportTwo.currentFailsafe = data.value("Curr FS").toInt() / 1000.00; reportTwo.timeoutFailsafe = data.value("Tmo FS").toInt(); reportTwo.setEnergy = data.value("Setenergy").toInt() / 10000.00; reportTwo.output = data.value("Output").toInt(); reportTwo.input= data.value("Input").toInt(); reportTwo.serialNumber = data.value("Serial").toString(); reportTwo.seconds = data.value("Sec").toInt(); // Not documented: //"AuthON": 0 //"Authreq": 0 emit reportTwoReceived(reportTwo); } else if (id == 3) { // We received valid data from the address over the data link, so the wallbox must be reachable setReachable(true); ReportThree reportThree; //qCDebug(dcKeba()) << "Report 3 received"; reportThree.currentPhase1 = data.value("I1").toInt() / 1000.00; reportThree.currentPhase2 = data.value("I2").toInt() / 1000.00; reportThree.currentPhase3 = data.value("I3").toInt() / 1000.00; reportThree.voltagePhase1 = data.value("U1").toInt(); reportThree.voltagePhase2 = data.value("U2").toInt(); reportThree.voltagePhase3 = data.value("U3").toInt(); reportThree.power = data.value("P").toInt() / 1000.00; reportThree.powerFactor = data.value("PF").toInt() / 10.00; reportThree.energySession = data.value("E pres").toInt() / 10000.00; reportThree.energyTotal = data.value("E total").toInt() / 10000.00; reportThree.serialNumber = data.value("Serial").toString(); reportThree.seconds = data.value("Sec").toInt(); emit reportThreeReceived(reportThree); } else if (id >= 100) { // We received valid data from the address over the data link, so the wallbox must be reachable setReachable(true); Report1XX report; //qCDebug(dcKeba()) << "Report" << id << "received"; report.sessionId = data.value("Session ID").toInt(); report.currHW = data.value("Curr HW").toInt(); report.startEnergy = data.value("E start").toInt() / 10000.00; report.presentEnergy = data.value("E pres").toInt() / 10000.00; report.startTime = data.value("started[s]").toInt(); report.endTime = data.value("ended[s]").toInt(); report.stopReason = data.value("reason").toInt(); report.rfidTag = data.value("RFID tag").toByteArray(); report.rfidClass = data.value("RFID class").toByteArray(); report.serialNumber = data.value("Serial").toString(); report.seconds = data.value("Sec").toInt(); emit report1XXReceived(id, report); } } else { // Broadcast message, lets see what we recognize if (data.contains("State")) { // We received valid data from the address over the data link, so the wallbox must be reachable setReachable(true); emit broadcastReceived(BroadcastType::BroadcastTypeState, data.value("State")); } if (data.contains("Plug")) { // We received valid data from the address over the data link, so the wallbox must be reachable setReachable(true); emit broadcastReceived(BroadcastType::BroadcastTypePlug, data.value("Plug")); } if (data.contains("Input")) { // We received valid data from the address over the data link, so the wallbox must be reachable setReachable(true); emit broadcastReceived(BroadcastType::BroadcastTypeInput, data.value("Input")); } if (data.contains("Enable sys")) { // We received valid data from the address over the data link, so the wallbox must be reachable setReachable(true); emit broadcastReceived(BroadcastType::BroadcastTypeEnableSys, data.value("Enable sys")); } if (data.contains("Max curr")) { // We received valid data from the address over the data link, so the wallbox must be reachable setReachable(true); emit broadcastReceived(BroadcastType::BroadcastTypeMaxCurr, data.value("Max curr")); } if (data.contains("E pres")) { // We received valid data from the address over the data link, so the wallbox must be reachable setReachable(true); emit broadcastReceived(BroadcastType::BroadcastTypeEPres, data.value("E pres")); } } } }