EVBox: Add support for chaining multiple Elvis
parent
1b95550ce8
commit
24ca3d225d
|
|
@ -3,7 +3,11 @@ include(../plugins.pri)
|
||||||
QT += network serialport
|
QT += network serialport
|
||||||
|
|
||||||
SOURCES += \
|
SOURCES += \
|
||||||
|
evboxdiscovery.cpp \
|
||||||
|
evboxport.cpp \
|
||||||
integrationpluginevbox.cpp \
|
integrationpluginevbox.cpp \
|
||||||
|
|
||||||
HEADERS += \
|
HEADERS += \
|
||||||
|
evboxdiscovery.h \
|
||||||
|
evboxport.h \
|
||||||
integrationpluginevbox.h \
|
integrationpluginevbox.h \
|
||||||
|
|
|
||||||
|
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||||
|
*
|
||||||
|
* For any further details and any questions please contact us under
|
||||||
|
* contact@nymea.io or see our FAQ/Licensing Information on
|
||||||
|
* https://nymea.io/license/faq
|
||||||
|
*
|
||||||
|
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
|
||||||
|
|
||||||
|
#include "evboxdiscovery.h"
|
||||||
|
|
||||||
|
EVBoxDiscovery::EVBoxDiscovery(EVBoxPort *evboxPort, QObject *parent)
|
||||||
|
: QObject{parent},
|
||||||
|
m_port{evboxPort}
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||||
|
*
|
||||||
|
* For any further details and any questions please contact us under
|
||||||
|
* contact@nymea.io or see our FAQ/Licensing Information on
|
||||||
|
* https://nymea.io/license/faq
|
||||||
|
*
|
||||||
|
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
|
||||||
|
|
||||||
|
#ifndef EVBOXDISCOVERY_H
|
||||||
|
#define EVBOXDISCOVERY_H
|
||||||
|
|
||||||
|
#include <QObject>
|
||||||
|
#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
|
||||||
|
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||||
|
*
|
||||||
|
* For any further details and any questions please contact us under
|
||||||
|
* contact@nymea.io or see our FAQ/Licensing Information on
|
||||||
|
* https://nymea.io/license/faq
|
||||||
|
*
|
||||||
|
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
|
||||||
|
|
||||||
|
#include "evboxport.h"
|
||||||
|
|
||||||
|
#include <QDataStream>
|
||||||
|
|
||||||
|
#include "extern-plugininfo.h"
|
||||||
|
|
||||||
|
|
||||||
|
#include <QTimer>
|
||||||
|
|
||||||
|
#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<void(QSerialPort::*)(QSerialPort::SerialPortError)>(&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<Command>(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<quint8>(STX);
|
||||||
|
stream.writeRawData(commandData.data(), commandData.length());
|
||||||
|
stream << static_cast<quint8>(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();
|
||||||
|
}
|
||||||
|
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||||
|
*
|
||||||
|
* For any further details and any questions please contact us under
|
||||||
|
* contact@nymea.io or see our FAQ/Licensing Information on
|
||||||
|
* https://nymea.io/license/faq
|
||||||
|
*
|
||||||
|
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
|
||||||
|
|
||||||
|
#ifndef EVBOXPORT_H
|
||||||
|
#define EVBOXPORT_H
|
||||||
|
|
||||||
|
#include <QObject>
|
||||||
|
#include <QSerialPort>
|
||||||
|
#include <QQueue>
|
||||||
|
#include <QTimer>
|
||||||
|
|
||||||
|
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<CommandWrapper> m_commandQueue;
|
||||||
|
QTimer m_waitTimer;
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif // EVBOXPORT_H
|
||||||
|
|
@ -32,6 +32,7 @@
|
||||||
#include "integrationpluginevbox.h"
|
#include "integrationpluginevbox.h"
|
||||||
#include "plugininfo.h"
|
#include "plugininfo.h"
|
||||||
#include "plugintimer.h"
|
#include "plugintimer.h"
|
||||||
|
#include "evboxport.h"
|
||||||
|
|
||||||
#include <QSerialPortInfo>
|
#include <QSerialPortInfo>
|
||||||
#include <QSerialPort>
|
#include <QSerialPort>
|
||||||
|
|
@ -51,279 +52,135 @@ IntegrationPluginEVBox::~IntegrationPluginEVBox()
|
||||||
|
|
||||||
void IntegrationPluginEVBox::discoverThings(ThingDiscoveryInfo *info)
|
void IntegrationPluginEVBox::discoverThings(ThingDiscoveryInfo *info)
|
||||||
{
|
{
|
||||||
// Create the list of available serial interfaces
|
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."));
|
||||||
foreach(QSerialPortInfo port, QSerialPortInfo::availablePorts()) {
|
return;
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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)
|
void IntegrationPluginEVBox::setupThing(ThingSetupInfo *info)
|
||||||
{
|
{
|
||||||
Thing *thing = info->thing();
|
Thing *thing = info->thing();
|
||||||
QString interface = thing->paramValue(evboxThingSerialPortParamTypeId).toString();
|
QString portName = thing->paramValue(evboxThingSerialPortParamTypeId).toString();
|
||||||
QSerialPort *serialPort = new QSerialPort(interface, info->thing());
|
QString serialNumber = thing->paramValue(evboxThingSerialNumberParamTypeId).toString();
|
||||||
|
|
||||||
serialPort->setBaudRate(QSerialPort::Baud38400);
|
// Opening the port, sharing with others if already opened.
|
||||||
serialPort->setDataBits(QSerialPort::Data8);
|
EVBoxPort *port = m_ports.value(portName);
|
||||||
serialPort->setStopBits(QSerialPort::OneStop);
|
if (!port) {
|
||||||
serialPort->setParity(QSerialPort::NoParity);
|
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);
|
thing->setStateValue(evboxConnectedStateTypeId, true);
|
||||||
QByteArray data = serialPort->readAll();
|
finishPendingAction(thing);
|
||||||
// qCDebug(dcEVBox()) << "Data received from serial port:" << data;
|
m_waitingForResponses[thing] = false;
|
||||||
m_inputBuffers[thing].append(data);
|
|
||||||
processInputBuffer(thing);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
connect(serialPort, static_cast<void(QSerialPort::*)(QSerialPort::SerialPortError)>(&QSerialPort::error), thing, [=](){
|
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){
|
||||||
qCWarning(dcEVBox()) << "Serial Port error" << serialPort->error() << serialPort->errorString();
|
if (serial != serialNumber) {
|
||||||
if (serialPort->error() != QSerialPort::NoError) {
|
return;
|
||||||
if (serialPort->isOpen()) {
|
|
||||||
serialPort->close();
|
|
||||||
}
|
|
||||||
thing->setStateValue(evboxConnectedStateTypeId, false);
|
|
||||||
QTimer::singleShot(1000, this, [=](){
|
|
||||||
serialPort->open(QSerialPort::ReadWrite);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
});
|
thing->setStateValue(evboxConnectedStateTypeId, true);
|
||||||
|
finishPendingAction(thing);
|
||||||
if (!serialPort->open(QSerialPort::ReadWrite)) {
|
m_waitingForResponses[thing] = false;
|
||||||
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<quint8>(STX);
|
|
||||||
stream.writeRawData(commandData.data(), commandData.length());
|
|
||||||
stream << static_cast<quint8>(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);
|
|
||||||
|
|
||||||
double currentPower = (chargingCurrentL1 + chargingCurrentL2 + chargingCurrentL3) * 23;
|
double currentPower = (chargingCurrentL1 + chargingCurrentL2 + chargingCurrentL3) * 23;
|
||||||
thing->setStateValue(evboxCurrentPowerStateTypeId, currentPower);
|
thing->setStateValue(evboxCurrentPowerStateTypeId, currentPower);
|
||||||
|
thing->setStateMinMaxValues(evboxMaxChargingCurrentStateTypeId, minChargingCurrent / 10, maxChargingCurrent / 10);
|
||||||
thing->setStateValue(evboxTotalEnergyConsumedStateTypeId, totalEnergyConsumed / 1000.0);
|
thing->setStateValue(evboxTotalEnergyConsumedStateTypeId, totalEnergyConsumed / 1000.0);
|
||||||
|
|
||||||
thing->setStateValue(evboxChargingStateTypeId, currentPower > 0);
|
thing->setStateValue(evboxChargingStateTypeId, currentPower > 0);
|
||||||
|
|
||||||
int phaseCount = 0;
|
int phaseCount = 0;
|
||||||
|
|
@ -336,13 +193,92 @@ void IntegrationPluginEVBox::processDataPacket(Thing *thing, const QByteArray &p
|
||||||
if (chargingCurrentL3 > 0) {
|
if (chargingCurrentL3 > 0) {
|
||||||
phaseCount++;
|
phaseCount++;
|
||||||
}
|
}
|
||||||
// If all phases are on 0, we aren't charging and don't know how may phases are used...
|
// If all phases are on 0, we aren't charging and don't know how many phases are available...
|
||||||
// so only updating the count if we actually do know that at least one is charging.
|
// Only updating the count if we actually do know that at least one is charging.
|
||||||
if (phaseCount > 0) {
|
if (phaseCount > 0) {
|
||||||
thing->setStateValue(evboxPhaseCountStateTypeId, phaseCount);
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -33,8 +33,11 @@
|
||||||
|
|
||||||
#include "integrations/integrationplugin.h"
|
#include "integrations/integrationplugin.h"
|
||||||
|
|
||||||
|
#include "evboxport.h"
|
||||||
|
|
||||||
#include "extern-plugininfo.h"
|
#include "extern-plugininfo.h"
|
||||||
|
|
||||||
|
#include <plugintimer.h>
|
||||||
#include <QTimer>
|
#include <QTimer>
|
||||||
|
|
||||||
class QSerialPort;
|
class QSerialPort;
|
||||||
|
|
@ -47,37 +50,23 @@ class IntegrationPluginEVBox: public IntegrationPlugin
|
||||||
Q_INTERFACES(IntegrationPlugin)
|
Q_INTERFACES(IntegrationPlugin)
|
||||||
|
|
||||||
public:
|
public:
|
||||||
enum Command {
|
|
||||||
Command68 = 68,
|
|
||||||
Command69 = 69
|
|
||||||
};
|
|
||||||
Q_ENUM(Command)
|
|
||||||
|
|
||||||
explicit IntegrationPluginEVBox();
|
explicit IntegrationPluginEVBox();
|
||||||
~IntegrationPluginEVBox();
|
~IntegrationPluginEVBox();
|
||||||
|
|
||||||
void discoverThings(ThingDiscoveryInfo *info) override;
|
void discoverThings(ThingDiscoveryInfo *info) override;
|
||||||
void setupThing(ThingSetupInfo *info) override;
|
void setupThing(ThingSetupInfo *info) override;
|
||||||
|
void postSetupThing(Thing *thing) override;
|
||||||
void thingRemoved(Thing *thing) override;
|
void thingRemoved(Thing *thing) override;
|
||||||
void executeAction(ThingActionInfo *info) override;
|
void executeAction(ThingActionInfo *info) override;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
bool sendCommand(Thing *thing, Command command, quint16 maxChargingCurrent);
|
void finishPendingAction(Thing *thing);
|
||||||
|
|
||||||
QByteArray createChecksum(const QByteArray &data) const;
|
|
||||||
|
|
||||||
void processInputBuffer(Thing *thing);
|
|
||||||
void processDataPacket(Thing *thing, const QByteArray &packet);
|
|
||||||
|
|
||||||
private:
|
private:
|
||||||
QHash<Thing*, QSerialPort*> m_serialPorts;
|
QHash<QString, EVBoxPort*> m_ports;
|
||||||
QHash<Thing*, ThingSetupInfo*> m_pendingSetups;
|
|
||||||
QHash<Thing*, QList<ThingActionInfo*>> m_pendingActions;
|
QHash<Thing*, QList<ThingActionInfo*>> m_pendingActions;
|
||||||
|
|
||||||
QHash<Thing*, QByteArray> m_inputBuffers;
|
|
||||||
|
|
||||||
QHash<Thing*, QTimer*> m_timers;
|
|
||||||
QHash<Thing*, bool> m_waitingForResponses;
|
QHash<Thing*, bool> m_waitingForResponses;
|
||||||
|
PluginTimer *m_timer = nullptr;
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif // INTEGRATIONPLUGINEVBOX_H
|
#endif // INTEGRATIONPLUGINEVBOX_H
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,6 @@
|
||||||
"displayName": "Elvi",
|
"displayName": "Elvi",
|
||||||
"createMethods": ["discovery"],
|
"createMethods": ["discovery"],
|
||||||
"setupMethod": "justadd",
|
"setupMethod": "justadd",
|
||||||
"discoveryType": "weak",
|
|
||||||
"interfaces": [ "evcharger", "connectable" ],
|
"interfaces": [ "evcharger", "connectable" ],
|
||||||
"paramTypes": [
|
"paramTypes": [
|
||||||
{
|
{
|
||||||
|
|
@ -22,6 +21,12 @@
|
||||||
"name":"serialPort",
|
"name":"serialPort",
|
||||||
"displayName": "Serial port",
|
"displayName": "Serial port",
|
||||||
"type": "QString"
|
"type": "QString"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "abc607a7-7dc5-48d4-b3d0-1545ddc63592",
|
||||||
|
"name":"serialNumber",
|
||||||
|
"displayName": "Serial number",
|
||||||
|
"type": "QString"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"stateTypes": [
|
"stateTypes": [
|
||||||
|
|
@ -41,8 +46,7 @@
|
||||||
"displayNameAction": "Enable/disable charging",
|
"displayNameAction": "Enable/disable charging",
|
||||||
"type": "bool",
|
"type": "bool",
|
||||||
"defaultValue": false,
|
"defaultValue": false,
|
||||||
"writable": true,
|
"writable": true
|
||||||
"cached": false
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "cc9ae86d-fc86-473f-ae90-d9eb20d7a011",
|
"id": "cc9ae86d-fc86-473f-ae90-d9eb20d7a011",
|
||||||
|
|
@ -53,9 +57,8 @@
|
||||||
"writable": true,
|
"writable": true,
|
||||||
"unit": "Ampere",
|
"unit": "Ampere",
|
||||||
"minValue": "6",
|
"minValue": "6",
|
||||||
"maxValue": "22",
|
"maxValue": "32",
|
||||||
"defaultValue": 6,
|
"defaultValue": 6
|
||||||
"cached": false
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "8d3c80b7-f1f1-48de-8b7a-f99b9bc688b7",
|
"id": "8d3c80b7-f1f1-48de-8b7a-f99b9bc688b7",
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue