From 24ca3d225d35f79ab70a6829be721ae1a78fe1e1 Mon Sep 17 00:00:00 2001 From: Michael Zanetti Date: Tue, 17 Jan 2023 15:38:44 +0100 Subject: [PATCH] EVBox: Add support for chaining multiple Elvis --- evbox/evbox.pro | 4 + evbox/evboxdiscovery.cpp | 39 +++ evbox/evboxdiscovery.h | 51 ++++ evbox/evboxport.cpp | 313 ++++++++++++++++++++ evbox/evboxport.h | 86 ++++++ evbox/integrationpluginevbox.cpp | 458 +++++++++++++----------------- evbox/integrationpluginevbox.h | 25 +- evbox/integrationpluginevbox.json | 15 +- 8 files changed, 706 insertions(+), 285 deletions(-) create mode 100644 evbox/evboxdiscovery.cpp create mode 100644 evbox/evboxdiscovery.h create mode 100644 evbox/evboxport.cpp create mode 100644 evbox/evboxport.h diff --git a/evbox/evbox.pro b/evbox/evbox.pro index 2126f7ef..e149ca78 100644 --- a/evbox/evbox.pro +++ b/evbox/evbox.pro @@ -3,7 +3,11 @@ include(../plugins.pri) QT += network serialport SOURCES += \ + evboxdiscovery.cpp \ + evboxport.cpp \ integrationpluginevbox.cpp \ HEADERS += \ + evboxdiscovery.h \ + evboxport.h \ integrationpluginevbox.h \ diff --git a/evbox/evboxdiscovery.cpp b/evbox/evboxdiscovery.cpp new file mode 100644 index 00000000..72787fb5 --- /dev/null +++ b/evbox/evboxdiscovery.cpp @@ -0,0 +1,39 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* Copyright 2013 - 2023, 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 "evboxdiscovery.h" + +EVBoxDiscovery::EVBoxDiscovery(EVBoxPort *evboxPort, QObject *parent) + : QObject{parent}, + m_port{evboxPort} +{ + +} + diff --git a/evbox/evboxdiscovery.h b/evbox/evboxdiscovery.h new file mode 100644 index 00000000..edcffd14 --- /dev/null +++ b/evbox/evboxdiscovery.h @@ -0,0 +1,51 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* Copyright 2013 - 2023, 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 +* +* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +#ifndef EVBOXDISCOVERY_H +#define EVBOXDISCOVERY_H + +#include +#include "evboxport.h" + +class EVBoxDiscovery : public QObject +{ + Q_OBJECT +public: + explicit EVBoxDiscovery(EVBoxPort *evboxPort, QObject *parent = nullptr); + +signals: + + +private: + EVBoxPort *m_port = nullptr; + +}; + +#endif // EVBOXDISCOVERY_H diff --git a/evbox/evboxport.cpp b/evbox/evboxport.cpp new file mode 100644 index 00000000..7689e091 --- /dev/null +++ b/evbox/evboxport.cpp @@ -0,0 +1,313 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* Copyright 2013 - 2023, 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 "evboxport.h" + +#include + +#include "extern-plugininfo.h" + + +#include + +#define STX 0x02 +#define ETX 0x03 + + + +EVBoxPort::EVBoxPort(const QString &portName, QObject *parent) + : QObject{parent} +{ + m_serialPort = new QSerialPort(portName, this); + m_serialPort->setBaudRate(QSerialPort::Baud38400); + m_serialPort->setDataBits(QSerialPort::Data8); + m_serialPort->setStopBits(QSerialPort::OneStop); + m_serialPort->setParity(QSerialPort::NoParity); + + connect(m_serialPort, &QSerialPort::readyRead, this, &EVBoxPort::onReadyRead); + + connect(m_serialPort, static_cast(&QSerialPort::error), this, [=](){ + qCWarning(dcEVBox()) << "Serial Port error" << m_serialPort->error() << m_serialPort->errorString(); + if (m_serialPort->error() != QSerialPort::NoError) { + if (m_serialPort->isOpen()) { + m_serialPort->close(); + } + emit closed(); + + QTimer::singleShot(1000, this, [=](){ + open(); + }); + } + }); + + // As per spec we need to wait at least 100ms after anything happens on the bus + m_waitTimer.setSingleShot(true); + m_waitTimer.setInterval(150); + connect(&m_waitTimer, &QTimer::timeout, this, &EVBoxPort::processQueue); +} + +bool EVBoxPort::open() +{ + if (m_serialPort->open(QSerialPort::ReadWrite)) { + emit opened(); + return true; + } + return false; +} + +bool EVBoxPort::isOpen() +{ + return m_serialPort->isOpen(); +} + +void EVBoxPort::close() +{ + m_serialPort->close(); +} + +void EVBoxPort::sendCommand(Command command, quint16 timeout, quint16 maxChargingCurrent, const QString &serial) +{ + CommandWrapper cmd; + cmd.command = command; + cmd.timeout = timeout; + cmd.maxChargingCurrent = maxChargingCurrent; + cmd.serial = serial; + + m_commandQueue.enqueue(cmd); + + processQueue(); +} + +void EVBoxPort::onReadyRead() +{ + m_waitTimer.start(); + + m_inputBuffer.append(m_serialPort->readAll()); + + QByteArray packet; + QDataStream inputStream(m_inputBuffer); + QDataStream outputStream(&packet, QIODevice::WriteOnly); + bool startFound = false, endFound = false; + + while (!inputStream.atEnd()) { + quint8 byte; + inputStream >> byte; + if (!startFound) { + if (byte == STX) { + startFound = true; + continue; + } else { + qCWarning(dcEVBox()) << "Discarding byte not matching start of frame 0x" + QString::number(byte, 16); + continue; + } + } + + if (byte == ETX) { + endFound = true; + break; + } + + outputStream << byte; + } + + if (startFound && endFound) { + m_inputBuffer.remove(0, packet.length() + 2); + } else { + qCDebug(dcEVBox()) << "Data is incomplete... Waiting for more..."; + return; + } + + if (packet.length() < 2) { // In practice it'll be longer, but let's make sure we won't crash checking the checksum on erraneous data + qCWarning(dcEVBox()) << "Packet is too short. Discarding packet..."; + return; + } + + qCDebug(dcEVBox()) << "<--" << packet; + + processDataPacket(packet); + +} + +void EVBoxPort::processDataPacket(const QByteArray &packet) +{ + // The data is a mess of hex and dec values... We'll read the stream as hex but have to convert some back to decimal. + QDataStream stream(QByteArray::fromHex(packet)); + + quint8 from, to, commandId, chargeBoxModuleCount; + quint16 minPollInterval, maxChargingCurrent, minChargingCurrent, chargingCurrentL1, chargingCurrentL2, chargingCurrentL3, cosinePhiL1, cosinePhiL2, cosinePhiL3, voltageL1, voltageL2, voltageL3; + quint32 totalEnergyConsumed; + + stream >> from >> to >> commandId; + + // Command is transmitted in decimal + Command command = static_cast(QString::number(commandId, 16).toInt()); + + + QString serial; + + if (command == Command68) { + quint32 serialNumber; + stream >> serialNumber; + serial = QString::number(serialNumber, 16); + + stream >> minPollInterval >> maxChargingCurrent >> chargeBoxModuleCount; + + if (chargeBoxModuleCount > 0) { + stream >> minChargingCurrent + >> chargingCurrentL1 + >> chargingCurrentL2 + >> chargingCurrentL3 + >> cosinePhiL1 + >> cosinePhiL2 + >> cosinePhiL3 + >> totalEnergyConsumed + >> voltageL1 + >> voltageL2 + >> voltageL3; + } else { + qCDebug(dcEVBox()) << "No chargebox module data in packet!"; + emit shortResponseReceived(command, serial); + return; + } + + } else if (command == Command69) { + + stream >> minPollInterval >> maxChargingCurrent >> chargeBoxModuleCount; + + if (chargeBoxModuleCount > 0) { + stream >> minChargingCurrent + >> chargingCurrentL1 + >> chargingCurrentL2 + >> chargingCurrentL3 + >> cosinePhiL1 + >> cosinePhiL2 + >> cosinePhiL3 + >> totalEnergyConsumed; + } else { + qCDebug(dcEVBox()) << "No chargebox module data in packet!"; + emit shortResponseReceived(command, serial); + return; + } + } else { + qCWarning(dcEVBox()) << "Unknown command id. Cannot process packet."; + return; + } + + qCDebug(dcEVBox()) << "Data packet received: From:" << from + << "To:" << to + << "Command:" << command + << "Serial:" << serial + << "MinAmpere:" << minChargingCurrent + << "MaxAmpere:" << maxChargingCurrent + << "AmpereL1" << chargingCurrentL1 + << "AmpereL2" << chargingCurrentL2 + << "AmpereL3" << chargingCurrentL3 + << "Total" << totalEnergyConsumed; + emit responseReceived(command, serial, minChargingCurrent, maxChargingCurrent, chargingCurrentL1, chargingCurrentL2, chargingCurrentL3, totalEnergyConsumed); + +} + +void EVBoxPort::processQueue() +{ + if (m_commandQueue.isEmpty()) { + return; + } + if (m_waitTimer.isActive()) { + qCDebug(dcEVBox()) << "Line is busy. Waiting..."; + return; + } + + CommandWrapper cmd = m_commandQueue.takeFirst(); + + QByteArray commandData; + + commandData += "80"; // Dst addr + commandData += "A0"; // Sender address + commandData += QString::number(cmd.command); + + qCDebug(dcEVBox()) << "Sending command" << cmd.command << "to" << cmd.serial << "MaxCurrent:" << cmd.maxChargingCurrent; + + if (cmd.command == Command68) { + if (cmd.serial.length() != 8) { + qCCritical(dcEVBox()) << "Serial must be 8 characters. Cannot send command..."; + processQueue(); + return; + } + commandData += cmd.serial; + // The content of the “information module” is 16 bytes in size and not defined. ¯\_(ツ)_/¯ + commandData += "00112233445566778899AABBCCDDEEFF"; + + } else if (cmd.command == Command69) { + qCDebug(dcEVBox()) << "Using command 69"; + + } + + commandData += QString("%1").arg(cmd.maxChargingCurrent * 10, 4, 10, QChar('0')); + commandData += QString("%1").arg(cmd.maxChargingCurrent * 10, 4, 10, QChar('0')); + commandData += QString("%1").arg(cmd.maxChargingCurrent * 10, 4, 10, QChar('0')); + commandData += QString("%1").arg(cmd.timeout, 4, 10, QChar('0')); + // If we fail to refresh the wallbox after the timeout, it shall turn off, which is what we'll use as default + // when we don't know what its set to (as we can't read it). + // Hence we do *not* cache the power and maxChargingCurrent states for this one + commandData += QString("%1").arg(6, 4, 10, QChar('0')); + commandData += QString("%1").arg(6, 4, 10, QChar('0')); + commandData += QString("%1").arg(6, 4, 10, QChar('0')); + + commandData += createChecksum(commandData); + + QByteArray data; + QDataStream stream(&data, QIODevice::WriteOnly); + stream << static_cast(STX); + stream.writeRawData(commandData.data(), commandData.length()); + stream << static_cast(ETX); + + qCDebug(dcEVBox()) << "-->" << data; // << "Hex:" << data.toHex(); + + qint64 count = m_serialPort->write(data); + if (count != data.length()) { + qCWarning(dcEVBox()) << "Error writing data to serial port:" << m_serialPort->errorString(); + } + + m_waitTimer.start(); +} + +QByteArray EVBoxPort::createChecksum(const QByteArray &data) const +{ + QDataStream checksumStream(data); + quint8 sum = 0; + quint8 xOr = 0; + while (!checksumStream.atEnd()) { + quint8 byte; + checksumStream >> byte; + sum += byte; + xOr ^= byte; + } + return QString("%1%2").arg(sum,2,16, QChar('0')).arg(xOr,2,16, QChar('0')).toUpper().toLocal8Bit(); +} diff --git a/evbox/evboxport.h b/evbox/evboxport.h new file mode 100644 index 00000000..ff4e875e --- /dev/null +++ b/evbox/evboxport.h @@ -0,0 +1,86 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* Copyright 2013 - 2023, 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 +* +* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +#ifndef EVBOXPORT_H +#define EVBOXPORT_H + +#include +#include +#include +#include + +class EVBoxPort : public QObject +{ + Q_OBJECT +public: + enum Command { + Command68 = 68, + Command69 = 69 + }; + Q_ENUM(Command) + + explicit EVBoxPort(const QString &portName, QObject *parent = nullptr); + + bool open(); + bool isOpen(); + void close(); + + void sendCommand(Command command, quint16 timeout, quint16 maxChargingCurrent, const QString &serial = "00000000"); + +signals: + void opened(); + void closed(); + void shortResponseReceived(EVBoxPort::Command command, const QString &serial); + void responseReceived(EVBoxPort::Command command, const QString &serial, quint16 minChargingCurrent, quint16 maxChargingCurrent, quint16 chargingCurrentL1, quint16 chargingCurrentL2, quint16 chargingCurrentL3, quint32 totalEnergyConsumed); + +private slots: + void processQueue(); + void onReadyRead(); + void processDataPacket(const QByteArray &packet); + +private: + QByteArray createChecksum(const QByteArray &data) const; + +private: + QSerialPort *m_serialPort = nullptr; + + QByteArray m_inputBuffer; + + struct CommandWrapper { + Command command = Command68; + QString serial; + quint16 timeout = 0; + quint16 maxChargingCurrent = 0; + }; + QQueue m_commandQueue; + QTimer m_waitTimer; +}; + +#endif // EVBOXPORT_H diff --git a/evbox/integrationpluginevbox.cpp b/evbox/integrationpluginevbox.cpp index 9f1188f0..9dba178f 100644 --- a/evbox/integrationpluginevbox.cpp +++ b/evbox/integrationpluginevbox.cpp @@ -32,6 +32,7 @@ #include "integrationpluginevbox.h" #include "plugininfo.h" #include "plugintimer.h" +#include "evboxport.h" #include #include @@ -51,279 +52,135 @@ IntegrationPluginEVBox::~IntegrationPluginEVBox() void IntegrationPluginEVBox::discoverThings(ThingDiscoveryInfo *info) { - // Create the list of available serial interfaces - - foreach(QSerialPortInfo port, QSerialPortInfo::availablePorts()) { - - qCDebug(dcEVBox()) << "Found serial port:" << port.portName(); - QString description = port.portName() + " " + port.manufacturer() + " " + port.description(); - ThingDescriptor thingDescriptor(info->thingClassId(), "EVBox Elvi", description); - ParamList parameters; - foreach (Thing *existingThing, myThings()) { - if (existingThing->paramValue(evboxThingSerialPortParamTypeId).toString() == port.portName()) { - thingDescriptor.setThingId(existingThing->id()); - break; - } - } - parameters.append(Param(evboxThingSerialPortParamTypeId, port.portName())); - thingDescriptor.setParams(parameters); - info->addThingDescriptor(thingDescriptor); + if (QSerialPortInfo::availablePorts().isEmpty()) { + info->finish(Thing::ThingErrorHardwareNotAvailable, QT_TR_NOOP("No serial ports are available on this system. Please connect a RS485 adapter first.")); + return; } - info->finish(Thing::ThingErrorNoError); + int discoveryCount = 0; + + foreach(const QSerialPortInfo &portInfo, QSerialPortInfo::availablePorts()) { + // Reusing existing ports as multiple boxes could be connected to a single adapter + EVBoxPort *port = m_ports.value(portInfo.portName()); + if (!port) { + // But if we don't have one yet, create it just for the discovery and delete it when the discovery info ends + port = new EVBoxPort(portInfo.portName(), info); + if (!port->open()) { + qCWarning(dcEVBox()) << "Unable to open serial port" << portInfo.portName() << "for discovery."; + delete port; + continue; + } + qCInfo(dcEVBox()) << "Serial port" << portInfo.portName() << "opened for discovery."; + } else { + qCDebug(dcEVBox()) << "Discovering on already open serial port:" << portInfo.portName(); + } + + discoveryCount++; + port->sendCommand(EVBoxPort::Command68, 10, 1); + + connect(port, &EVBoxPort::responseReceived, info, [=](EVBoxPort::Command command, const QString &serial){ + if (command != EVBoxPort::Command68) { + return; + } + + ThingDescriptor thingDescriptor(info->thingClassId(), "EVBox Elvi", serial); + ParamList params{ + {evboxThingSerialPortParamTypeId, portInfo.portName()}, + {evboxThingSerialNumberParamTypeId, serial} + }; + thingDescriptor.setParams(params); + Thing *existingThing = myThings().findByParams(params); + if (existingThing) { + thingDescriptor.setThingId(existingThing->id()); + } + + if (!info->property("foundSerials").toStringList().contains(serial)) { + qCInfo(dcEVBox()) << "Adding descriptor for port" << portInfo.portName() << "Serial:" << serial << "Existing:" << (existingThing != nullptr ? "yes" : "no"); + info->addThingDescriptor(thingDescriptor); + info->setProperty("foundSerials", QStringList{serial} + info->property("foundSerials").toStringList()); + } else { + qCInfo(dcEVBox()) << "Discarding duplicate descriptor for port" << portInfo.portName() << "Serial:" << serial; + } + }); + } + + if (discoveryCount == 0) { + info->finish(Thing::ThingErrorHardwareFailure, QT_TR_NOOP("Unable to open the RS485 port. Please make sure the RS485 adapter is connected properly and not in use by anything else.")); + return; + } + + QTimer::singleShot(3000, info, [info](){ + info->finish(Thing::ThingErrorNoError); + }); } void IntegrationPluginEVBox::setupThing(ThingSetupInfo *info) { Thing *thing = info->thing(); - QString interface = thing->paramValue(evboxThingSerialPortParamTypeId).toString(); - QSerialPort *serialPort = new QSerialPort(interface, info->thing()); + QString portName = thing->paramValue(evboxThingSerialPortParamTypeId).toString(); + QString serialNumber = thing->paramValue(evboxThingSerialNumberParamTypeId).toString(); - serialPort->setBaudRate(QSerialPort::Baud38400); - serialPort->setDataBits(QSerialPort::Data8); - serialPort->setStopBits(QSerialPort::OneStop); - serialPort->setParity(QSerialPort::NoParity); + // Opening the port, sharing with others if already opened. + EVBoxPort *port = m_ports.value(portName); + if (!port) { + qCInfo(dcEVBox()) << "Port" << portName << "not open yet. Opening."; + port = new EVBoxPort(portName, this); + if (!port->open()) { + qCWarning(dcEVBox()) << "Unable to open port" << portName << "for EVBox" << serialNumber; + delete port; + info->finish(Thing::ThingErrorHardwareFailure, QT_TR_NOOP("Unable to open the RS485 port. Please make sure the RS485 adapter is connected properly.")); + return; + } + m_ports.insert(portName, port); + } - connect(serialPort, &QSerialPort::readyRead, thing, [=]() { + + + // Setup routine: Try to set the max charging current to 6A and see if we get a valid answer + port->sendCommand(EVBoxPort::Command68, 60, 6, serialNumber); + connect(port, &EVBoxPort::closed, info, [info](){ + info->finish(Thing::ThingErrorHardwareFailure, QT_TR_NOOP("The EVBox is not responding.")); + }); + connect(port, &EVBoxPort::responseReceived, info, [info, serialNumber](EVBoxPort::Command /*command*/, const QString &serial){ + if (serial == serialNumber) { + info->finish(Thing::ThingErrorNoError); + } + }); + QTimer::singleShot(3000, info, [info](){ + info->finish(Thing::ThingErrorTimeout, QT_TR_NOOP("The EVBox is not responding.")); + }); + + + // And connect the signals to the thing for when the setup went well + connect(port, &EVBoxPort::closed, thing, [thing, portName](){ + qCInfo(dcEVBox()) << "Port" << portName << "closed. Marking thing as offline:" << thing->name(); + thing->setStateValue(evboxConnectedStateTypeId, false); + }); + connect(port, &EVBoxPort::opened, thing, [portName](){ + qCInfo(dcEVBox()) << "Port" << portName << "opened."; + }); + + connect(port, &EVBoxPort::shortResponseReceived, thing, [this, thing, serialNumber](EVBoxPort::Command /*command*/, const QString &serial){ + if (serial != serialNumber) { + return; + } thing->setStateValue(evboxConnectedStateTypeId, true); - QByteArray data = serialPort->readAll(); -// qCDebug(dcEVBox()) << "Data received from serial port:" << data; - m_inputBuffers[thing].append(data); - processInputBuffer(thing); + finishPendingAction(thing); + m_waitingForResponses[thing] = false; }); - connect(serialPort, static_cast(&QSerialPort::error), thing, [=](){ - qCWarning(dcEVBox()) << "Serial Port error" << serialPort->error() << serialPort->errorString(); - if (serialPort->error() != QSerialPort::NoError) { - if (serialPort->isOpen()) { - serialPort->close(); - } - thing->setStateValue(evboxConnectedStateTypeId, false); - QTimer::singleShot(1000, this, [=](){ - serialPort->open(QSerialPort::ReadWrite); - }); + connect(port, &EVBoxPort::responseReceived, thing, [this, thing, serialNumber](EVBoxPort::Command /*command*/, const QString &serial, quint16 minChargingCurrent, quint16 maxChargingCurrent, quint16 chargingCurrentL1, quint16 chargingCurrentL2, quint16 chargingCurrentL3, quint32 totalEnergyConsumed){ + if (serial != serialNumber) { + return; } - }); - - if (!serialPort->open(QSerialPort::ReadWrite)) { - qCWarning(dcEVBox()) << "Unable to open serial port"; - info->finish(Thing::ThingErrorHardwareFailure, QT_TR_NOOP("Unable to open the RS485 port. Please make sure the RS485 adapter is connected properly.")); - return; - } - - m_serialPorts.insert(thing, serialPort); - - m_pendingSetups.insert(thing, info); - connect(info, &ThingSetupInfo::finished, this, [=](){ - m_pendingSetups.remove(thing); - }); - QTimer::singleShot(2000, info, [=](){ - qCDebug(dcEVBox()) << "Timeout during setup"; - delete m_serialPorts.take(info->thing()); - info->finish(Thing::ThingErrorHardwareNotAvailable, QT_TR_NOOP("The EVBox is not responding.")); - }); - - - sendCommand(thing, Command69, 1); -} - -void IntegrationPluginEVBox::thingRemoved(Thing *thing) -{ - m_timers.remove(thing); - delete m_serialPorts.take(thing); -} - -void IntegrationPluginEVBox::executeAction(ThingActionInfo *info) -{ - Thing *thing = info->thing(); - - if (info->action().actionTypeId() == evboxPowerActionTypeId) { - bool power = info->action().paramValue(evboxPowerActionPowerParamTypeId).toBool(); - sendCommand(info->thing(), Command69, power ? info->thing()->stateValue(evboxMaxChargingCurrentStateTypeId).toUInt() : 0); - } else if (info->action().actionTypeId() == evboxMaxChargingCurrentActionTypeId) { - int maxChargingCurrent = info->action().paramValue(evboxMaxChargingCurrentActionMaxChargingCurrentParamTypeId).toInt(); - sendCommand(info->thing(), Command69, maxChargingCurrent); - } - - m_pendingActions[thing].append(info); - connect(info, &ThingActionInfo::finished, this, [=](){ - m_pendingActions[thing].removeAll(info); - }); - -} - -bool IntegrationPluginEVBox::sendCommand(Thing *thing, Command command, quint16 maxChargingCurrent) -{ - QByteArray commandData; - - commandData += "80"; // Dst addr - commandData += "A0"; // Sender address - commandData += QString::number(command); - commandData += QString("%1").arg(maxChargingCurrent * 10, 4, 10, QChar('0')); - commandData += QString("%1").arg(maxChargingCurrent * 10, 4, 10, QChar('0')); - commandData += QString("%1").arg(maxChargingCurrent * 10, 4, 10, QChar('0')); - commandData += "003C"; // Timeout (60 sec) - // If we fail to refresh the wallbox after the timeout, it shall turn off, which is what we'll use as default - // when we don't know what its set to (as we can't read it). - // Hence we do *not* cache the power and maxChargingCurrent states for this one - commandData += QString("%1").arg(0, 4, 10, QChar('0')); - commandData += QString("%1").arg(0, 4, 10, QChar('0')); - commandData += QString("%1").arg(0, 4, 10, QChar('0')); - - commandData += createChecksum(commandData); - - QByteArray data; - QDataStream stream(&data, QIODevice::WriteOnly); - stream << static_cast(STX); - stream.writeRawData(commandData.data(), commandData.length()); - stream << static_cast(ETX); - - qCDebug(dcEVBox()) << "Writing data:" << data << "->" << data.toHex(); - QSerialPort *serialPort = m_serialPorts.value(thing); - qint64 count = serialPort->write(data); - if (count == data.length()) { - m_waitingForResponses[thing] = true; - } - return count == data.length(); -} - -QByteArray IntegrationPluginEVBox::createChecksum(const QByteArray &data) const -{ - QDataStream checksumStream(data); - quint8 sum = 0; - quint8 xOr = 0; - while (!checksumStream.atEnd()) { - quint8 byte; - checksumStream >> byte; - sum += byte; - xOr ^= byte; - } - return QString("%1%2").arg(sum,2,16, QChar('0')).arg(xOr,2,16, QChar('0')).toUpper().toLocal8Bit(); -} - -void IntegrationPluginEVBox::processInputBuffer(Thing *thing) -{ - QByteArray packet; - QDataStream inputStream(m_inputBuffers.value(thing)); - QDataStream outputStream(&packet, QIODevice::WriteOnly); - bool startFound = false, endFound = false; - - while (!inputStream.atEnd()) { - quint8 byte; - inputStream >> byte; - if (!startFound) { - if (byte == STX) { - startFound = true; - continue; - } else { - qCWarning(dcEVBox()) << "Discarding byte not matching start of frame 0x" + QString::number(byte, 16); - continue; - } - } - - if (byte == ETX) { - endFound = true; - break; - } - - outputStream << byte; - } - - if (startFound && endFound) { - m_inputBuffers[thing].remove(0, packet.length() + 2); - } else { -// qCDebug(dcEVBox()) << "Data is incomplete... Waiting for more..."; - return; - } - - if (packet.length() < 2) { // In practice it'll be longer, but let's make sure we won't crash checking the checksum on erraneous data - qCWarning(dcEVBox()) << "Packet is too short. Discarding packet..."; - return; - } - - qCDebug(dcEVBox()) << "Packet received:" << packet; - - QByteArray checksum = createChecksum(packet.left(packet.length() - 4)); - if (checksum != packet.right(4)) { - qCWarning(dcEVBox()) << "Checksum mismatch for incoming packet:" << packet << "Given checksum:" << packet.right(4) << "Expected:" << checksum; - return; - } - - // We received something valid... Assuming the last command we've sent is OK. - // There's no way to properly match a response to a command, so... - if (m_pendingSetups.contains(thing)) { - qCDebug(dcEVBox()) << "Finishing setup"; - - // Can't use a pluginTimer because it may collide with data on the wire, so we're - // manually re-starting the timer whenever we receive something. - QTimer *timer = new QTimer(thing); - m_timers.insert(thing, timer); - timer->setInterval(1000); - - connect(timer, &QTimer::timeout, thing, [=](){ - thing->setStateValue(evboxConnectedStateTypeId, !m_waitingForResponses[thing]); - - if (thing->stateValue(evboxPowerStateTypeId).toBool()) { - sendCommand(thing, Command69, thing->stateValue(evboxMaxChargingCurrentStateTypeId).toDouble()); - } else { - sendCommand(thing, Command69, 0); - } - }); - - m_pendingSetups.take(thing)->finish(Thing::ThingErrorNoError); - } - if (!m_pendingActions.value(thing).isEmpty()) { - ThingActionInfo *info = m_pendingActions.value(thing).first(); - if (info->action().actionTypeId() == evboxPowerActionTypeId) { - thing->setStateValue(evboxPowerStateTypeId, info->action().paramValue(evboxPowerActionPowerParamTypeId)); - } else if (info->action().actionTypeId() == evboxMaxChargingCurrentActionTypeId) { - thing->setStateValue(evboxMaxChargingCurrentStateTypeId, info->action().paramValue(evboxMaxChargingCurrentActionMaxChargingCurrentParamTypeId)); - } - info->finish(Thing::ThingErrorNoError); - } - - processDataPacket(thing, packet); -} - -void IntegrationPluginEVBox::processDataPacket(Thing *thing, const QByteArray &packet) -{ - - // The data is a mess of hex and dec values... So do not wonder about the weird from/to hex mess in here... - QDataStream stream(QByteArray::fromHex(packet)); - - quint8 from, to, commandId, wallboxCount; - quint16 minPollInterval, maxChargingCurrent; - stream >> from >> to >> commandId >> minPollInterval >> maxChargingCurrent >> wallboxCount; - - commandId = QString::number(commandId, 16).toInt(); - - qCDebug(dcEVBox()) << QString("From: %1, To: %2, CMD: %3, MinPollInterval: %4, maxChargingCurrent: %5, Wallbox data count: %6") - .arg(from).arg(to).arg(commandId).arg(minPollInterval).arg(maxChargingCurrent).arg(wallboxCount); - - if (commandId != Command69) { - qCWarning(dcEVBox()) << "Only command 69 is implemented! Adjust response parsing if sending other commands."; - return; - } - - m_waitingForResponses[thing] = false; - - // Command 69 would give a list of wallboxes (they can be chained apparently) but we only support a single one for now -// for (int i = 0; i < wallboxCount; i++) { - - if (wallboxCount > 0) { - quint16 minChargingCurrent, chargingCurrentL1, chargingCurrentL2, chargingCurrentL3, cosinePhiL1, cosinePhiL2, cosinePhiL3, totalEnergyConsumed; - stream >> minChargingCurrent >> chargingCurrentL1 >> chargingCurrentL2 >> chargingCurrentL3 >> cosinePhiL1 >> cosinePhiL2 >> cosinePhiL3 >> totalEnergyConsumed; - - qCDebug(dcEVBox()) << QString("Min current: %1, actual current L1: %2, L2: %3, L3: %4, Total energy: %5") - .arg(minChargingCurrent).arg(chargingCurrentL1).arg(chargingCurrentL2).arg(chargingCurrentL3).arg(totalEnergyConsumed); - - thing->setStateMinMaxValues(evboxMaxChargingCurrentStateTypeId, minChargingCurrent / 10, maxChargingCurrent / 10); + thing->setStateValue(evboxConnectedStateTypeId, true); + finishPendingAction(thing); + m_waitingForResponses[thing] = false; double currentPower = (chargingCurrentL1 + chargingCurrentL2 + chargingCurrentL3) * 23; thing->setStateValue(evboxCurrentPowerStateTypeId, currentPower); - + thing->setStateMinMaxValues(evboxMaxChargingCurrentStateTypeId, minChargingCurrent / 10, maxChargingCurrent / 10); thing->setStateValue(evboxTotalEnergyConsumedStateTypeId, totalEnergyConsumed / 1000.0); - thing->setStateValue(evboxChargingStateTypeId, currentPower > 0); int phaseCount = 0; @@ -336,13 +193,92 @@ void IntegrationPluginEVBox::processDataPacket(Thing *thing, const QByteArray &p if (chargingCurrentL3 > 0) { phaseCount++; } - // If all phases are on 0, we aren't charging and don't know how may phases are used... - // so only updating the count if we actually do know that at least one is charging. + // If all phases are on 0, we aren't charging and don't know how many phases are available... + // Only updating the count if we actually do know that at least one is charging. if (phaseCount > 0) { thing->setStateValue(evboxPhaseCountStateTypeId, phaseCount); } - } - - m_timers.value(thing)->start(); + }); +} + +void IntegrationPluginEVBox::postSetupThing(Thing */*thing*/) +{ + if (!m_timer) { + m_timer = hardwareManager()->pluginTimerManager()->registerTimer(5); + connect(m_timer, &PluginTimer::timeout, this, [this](){ + foreach (Thing *t, myThings()) { + QString portName = t->paramValue(evboxThingSerialPortParamTypeId).toString(); + QString serial = t->paramValue(evboxThingSerialNumberParamTypeId).toString(); + + if (m_waitingForResponses.value(t)) { + qCInfo(dcEVBox()) << "Wallbox" << t->name() << "did not respond to last command. Marking offline."; + t->setStateValue(evboxConnectedStateTypeId, false); + } + + EVBoxPort *port = m_ports.value(portName); + if (port->isOpen()) { + quint16 maxChargingCurrent = 0; + if (t->stateValue(evboxPowerStateTypeId).toBool()) { + maxChargingCurrent = t->stateValue(evboxMaxChargingCurrentStateTypeId).toUInt(); + } + port->sendCommand(EVBoxPort::Command68, 60, maxChargingCurrent, serial); + m_waitingForResponses[t] = true; + } + } + }); + } +} + +void IntegrationPluginEVBox::thingRemoved(Thing *thing) +{ + QString portName = thing->paramValue(evboxThingSerialPortParamTypeId).toString(); + if (myThings().filterByParam(evboxThingSerialPortParamTypeId, portName).isEmpty()) { + qCInfo(dcEVBox()).nospace() << "No more EVBox devices using port " << portName << ". Destroying port."; + delete m_ports.take(portName); + } + + if (myThings().isEmpty()) { + hardwareManager()->pluginTimerManager()->unregisterTimer(m_timer); + m_timer = nullptr; + } +} + +void IntegrationPluginEVBox::executeAction(ThingActionInfo *info) +{ + Thing *thing = info->thing(); + + QString portName = thing->paramValue(evboxThingSerialPortParamTypeId).toString(); + QString serial = thing->paramValue(evboxThingSerialNumberParamTypeId).toString(); + EVBoxPort *port = m_ports.value(portName); + + qCDebug(dcEVBox()) << "Executing action" << info->action().actionTypeId().toString(); + if (info->action().actionTypeId() == evboxPowerActionTypeId) { + bool power = info->action().paramValue(evboxPowerActionPowerParamTypeId).toBool(); + quint16 maxChargingCurrent = thing->stateValue(evboxMaxChargingCurrentStateTypeId).toUInt(); + port->sendCommand(EVBoxPort::Command68, 60, power ? maxChargingCurrent : 0, serial); + } else if (info->action().actionTypeId() == evboxMaxChargingCurrentActionTypeId) { + int maxChargingCurrent = info->action().paramValue(evboxMaxChargingCurrentActionMaxChargingCurrentParamTypeId).toInt(); + port->sendCommand(EVBoxPort::Command68, 60, maxChargingCurrent, serial); + } + + m_pendingActions[thing].append(info); + connect(info, &ThingActionInfo::finished, this, [=](){ + m_pendingActions[thing].removeAll(info); + }); + +} + +void IntegrationPluginEVBox::finishPendingAction(Thing *thing) +{ + if (!m_pendingActions.value(thing).isEmpty()) { + ThingActionInfo *info = m_pendingActions.value(thing).first(); + qCDebug(dcEVBox()) << "Finishing action:" << info->action().actionTypeId().toString(); + if (info->action().actionTypeId() == evboxPowerActionTypeId) { + thing->setStateValue(evboxPowerStateTypeId, info->action().paramValue(evboxPowerActionPowerParamTypeId)); + } else if (info->action().actionTypeId() == evboxMaxChargingCurrentActionTypeId) { + thing->setStateValue(evboxMaxChargingCurrentStateTypeId, info->action().paramValue(evboxMaxChargingCurrentActionMaxChargingCurrentParamTypeId)); + } + info->finish(Thing::ThingErrorNoError); + } } diff --git a/evbox/integrationpluginevbox.h b/evbox/integrationpluginevbox.h index fe663d55..c1df0234 100644 --- a/evbox/integrationpluginevbox.h +++ b/evbox/integrationpluginevbox.h @@ -33,8 +33,11 @@ #include "integrations/integrationplugin.h" +#include "evboxport.h" + #include "extern-plugininfo.h" +#include #include class QSerialPort; @@ -47,37 +50,23 @@ class IntegrationPluginEVBox: public IntegrationPlugin Q_INTERFACES(IntegrationPlugin) public: - enum Command { - Command68 = 68, - Command69 = 69 - }; - Q_ENUM(Command) - explicit IntegrationPluginEVBox(); ~IntegrationPluginEVBox(); void discoverThings(ThingDiscoveryInfo *info) override; void setupThing(ThingSetupInfo *info) override; + void postSetupThing(Thing *thing) override; void thingRemoved(Thing *thing) override; void executeAction(ThingActionInfo *info) override; private: - bool sendCommand(Thing *thing, Command command, quint16 maxChargingCurrent); - - QByteArray createChecksum(const QByteArray &data) const; - - void processInputBuffer(Thing *thing); - void processDataPacket(Thing *thing, const QByteArray &packet); + void finishPendingAction(Thing *thing); private: - QHash m_serialPorts; - QHash m_pendingSetups; + QHash m_ports; QHash> m_pendingActions; - - QHash m_inputBuffers; - - QHash m_timers; QHash m_waitingForResponses; + PluginTimer *m_timer = nullptr; }; #endif // INTEGRATIONPLUGINEVBOX_H diff --git a/evbox/integrationpluginevbox.json b/evbox/integrationpluginevbox.json index 0a4b5b79..8952e7b2 100644 --- a/evbox/integrationpluginevbox.json +++ b/evbox/integrationpluginevbox.json @@ -14,7 +14,6 @@ "displayName": "Elvi", "createMethods": ["discovery"], "setupMethod": "justadd", - "discoveryType": "weak", "interfaces": [ "evcharger", "connectable" ], "paramTypes": [ { @@ -22,6 +21,12 @@ "name":"serialPort", "displayName": "Serial port", "type": "QString" + }, + { + "id": "abc607a7-7dc5-48d4-b3d0-1545ddc63592", + "name":"serialNumber", + "displayName": "Serial number", + "type": "QString" } ], "stateTypes": [ @@ -41,8 +46,7 @@ "displayNameAction": "Enable/disable charging", "type": "bool", "defaultValue": false, - "writable": true, - "cached": false + "writable": true }, { "id": "cc9ae86d-fc86-473f-ae90-d9eb20d7a011", @@ -53,9 +57,8 @@ "writable": true, "unit": "Ampere", "minValue": "6", - "maxValue": "22", - "defaultValue": 6, - "cached": false + "maxValue": "32", + "defaultValue": 6 }, { "id": "8d3c80b7-f1f1-48de-8b7a-f99b9bc688b7",