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",