Webasto: Add webasto NEXT support

This commit is contained in:
Simon Stürz 2023-04-03 12:13:23 +02:00
parent 83c22e3b62
commit 28f280b15c
11 changed files with 1531 additions and 126 deletions

View File

@ -58,9 +58,7 @@ ModbusTcpMaster::~ModbusTcpMaster()
m_reconnectTimer->stop(); m_reconnectTimer->stop();
} }
if (m_modbusTcpClient) { disconnectDevice();
disconnectDevice();
}
} }
QHostAddress ModbusTcpMaster::hostAddress() const QHostAddress ModbusTcpMaster::hostAddress() const
@ -78,19 +76,25 @@ void ModbusTcpMaster::setPort(uint port)
m_port = port; m_port = port;
} }
QString ModbusTcpMaster::connectionUrl() const
{
return QString("%1:%2").arg(m_hostAddress.toString()).arg(m_port);
}
void ModbusTcpMaster::setHostAddress(const QHostAddress &hostAddress) void ModbusTcpMaster::setHostAddress(const QHostAddress &hostAddress)
{ {
m_hostAddress = hostAddress; m_hostAddress = hostAddress;
} }
bool ModbusTcpMaster::connectDevice() { bool ModbusTcpMaster::connectDevice()
{
// TCP connection to target device // TCP connection to target device
if (!m_modbusTcpClient) if (!m_modbusTcpClient)
return false; return false;
// Only connect if we are in the unconnected state // Only connect if we are in the unconnected state
if (m_modbusTcpClient->state() == QModbusDevice::UnconnectedState) { if (m_modbusTcpClient->state() == QModbusDevice::UnconnectedState) {
qCDebug(dcModbusTcpMaster()) << "Connecting modbus TCP client to" << QString("%1:%2").arg(m_hostAddress.toString()).arg(m_port); qCDebug(dcModbusTcpMaster()) << "Connecting modbus TCP client to" << connectionUrl();
m_modbusTcpClient->setConnectionParameter(QModbusDevice::NetworkPortParameter, m_port); m_modbusTcpClient->setConnectionParameter(QModbusDevice::NetworkPortParameter, m_port);
m_modbusTcpClient->setConnectionParameter(QModbusDevice::NetworkAddressParameter, m_hostAddress.toString()); m_modbusTcpClient->setConnectionParameter(QModbusDevice::NetworkAddressParameter, m_hostAddress.toString());
m_modbusTcpClient->setTimeout(m_timeout); m_modbusTcpClient->setTimeout(m_timeout);
@ -98,9 +102,10 @@ bool ModbusTcpMaster::connectDevice() {
return m_modbusTcpClient->connectDevice(); return m_modbusTcpClient->connectDevice();
} else if (m_modbusTcpClient->state() != QModbusDevice::ConnectedState) { } else if (m_modbusTcpClient->state() != QModbusDevice::ConnectedState) {
// Restart the timer in case of connecting not finished yet or closing // Restart the timer in case of connecting not finished yet or closing
qCDebug(dcModbusTcpMaster()) << "Starting the reconnect mechanism timer";
m_reconnectTimer->start(); m_reconnectTimer->start();
} else { } else {
qCWarning(dcModbusTcpMaster()) << "Connect modbus TCP device" << QString("%1:%2").arg(m_hostAddress.toString()).arg(m_port) << "called, but the socket is currently in the" << m_modbusTcpClient->state(); qCWarning(dcModbusTcpMaster()) << "Connect modbus TCP device" << connectionUrl() << "called, but the socket is currently in the" << m_modbusTcpClient->state();
} }
return false; return false;
@ -108,9 +113,6 @@ bool ModbusTcpMaster::connectDevice() {
void ModbusTcpMaster::disconnectDevice() void ModbusTcpMaster::disconnectDevice()
{ {
if (!m_modbusTcpClient)
return;
// Stop the reconnect timer since disconnect was explicitly called // Stop the reconnect timer since disconnect was explicitly called
m_reconnectTimer->stop(); m_reconnectTimer->stop();
m_modbusTcpClient->disconnectDevice(); m_modbusTcpClient->disconnectDevice();
@ -118,10 +120,7 @@ void ModbusTcpMaster::disconnectDevice()
bool ModbusTcpMaster::reconnectDevice() bool ModbusTcpMaster::reconnectDevice()
{ {
qCWarning(dcModbusTcpMaster()) << "Reconnecting modbus TCP device" << QString("%1:%2").arg(m_hostAddress.toString()).arg(m_port); qCWarning(dcModbusTcpMaster()) << "Reconnecting modbus TCP device" << connectionUrl();
if (!m_modbusTcpClient)
return false;
disconnectDevice(); disconnectDevice();
return connectDevice(); return connectDevice();
} }
@ -165,10 +164,6 @@ QModbusDevice::Error ModbusTcpMaster::error() const
QUuid ModbusTcpMaster::readCoil(uint slaveAddress, uint registerAddress, uint size) QUuid ModbusTcpMaster::readCoil(uint slaveAddress, uint registerAddress, uint size)
{ {
if (!m_modbusTcpClient) {
return QUuid();
}
QUuid requestId = QUuid::createUuid(); QUuid requestId = QUuid::createUuid();
QModbusDataUnit request = QModbusDataUnit(QModbusDataUnit::RegisterType::Coils, registerAddress, size); QModbusDataUnit request = QModbusDataUnit(QModbusDataUnit::RegisterType::Coils, registerAddress, size);
@ -183,12 +178,12 @@ QUuid ModbusTcpMaster::readCoil(uint slaveAddress, uint registerAddress, uint si
emit receivedCoil(reply->serverAddress(), modbusAddress, unit.values()); emit receivedCoil(reply->serverAddress(), modbusAddress, unit.values());
} else { } else {
emit readRequestExecuted(requestId, false); emit readRequestExecuted(requestId, false);
qCWarning(dcModbusTcpMaster()) << "Read response error for device" << m_hostAddress.toString() << ":" << reply->error(); qCWarning(dcModbusTcpMaster()) << "Read response error for device" << connectionUrl() << ":" << reply->error();
} }
}); });
connect(reply, &QModbusReply::errorOccurred, this, [reply, requestId, this] (QModbusDevice::Error error){ connect(reply, &QModbusReply::errorOccurred, this, [reply, requestId, this] (QModbusDevice::Error error){
qCWarning(dcModbusTcpMaster()) << "Modbus reply error for device" << m_hostAddress.toString() << ":" << error; qCWarning(dcModbusTcpMaster()) << "Modbus reply error for device" << connectionUrl() << ":" << error;
emit readRequestError(requestId, reply->errorString()); emit readRequestError(requestId, reply->errorString());
}); });
@ -198,7 +193,7 @@ QUuid ModbusTcpMaster::readCoil(uint slaveAddress, uint registerAddress, uint si
return QUuid(); return QUuid();
} }
} else { } else {
qCWarning(dcModbusTcpMaster()) << "Read error for device" << m_hostAddress.toString() << ":" << m_modbusTcpClient->errorString(); qCWarning(dcModbusTcpMaster()) << "Read error for device" << connectionUrl() << ":" << m_modbusTcpClient->errorString();
return QUuid(); return QUuid();
} }
return requestId; return requestId;
@ -206,10 +201,6 @@ QUuid ModbusTcpMaster::readCoil(uint slaveAddress, uint registerAddress, uint si
QUuid ModbusTcpMaster::writeHoldingRegisters(uint slaveAddress, uint registerAddress, const QVector<quint16> &values) QUuid ModbusTcpMaster::writeHoldingRegisters(uint slaveAddress, uint registerAddress, const QVector<quint16> &values)
{ {
if (!m_modbusTcpClient) {
return QUuid();
}
QUuid requestId = QUuid::createUuid(); QUuid requestId = QUuid::createUuid();
QModbusDataUnit request = QModbusDataUnit(QModbusDataUnit::RegisterType::HoldingRegisters, registerAddress, values.length()); QModbusDataUnit request = QModbusDataUnit(QModbusDataUnit::RegisterType::HoldingRegisters, registerAddress, values.length());
request.setValues(values); request.setValues(values);
@ -224,13 +215,13 @@ QUuid ModbusTcpMaster::writeHoldingRegisters(uint slaveAddress, uint registerAdd
emit receivedHoldingRegister(reply->serverAddress(), modbusAddress, unit.values()); emit receivedHoldingRegister(reply->serverAddress(), modbusAddress, unit.values());
} else { } else {
emit writeRequestExecuted(requestId, false); emit writeRequestExecuted(requestId, false);
qCWarning(dcModbusTcpMaster()) << "Read response error for device" << m_hostAddress.toString() << ":" << reply->error(); qCWarning(dcModbusTcpMaster()) << "Read response error for device" << connectionUrl() << ":" << reply->error();
} }
reply->deleteLater(); reply->deleteLater();
}); });
connect(reply, &QModbusReply::errorOccurred, this, [reply, requestId, this] (QModbusDevice::Error error){ connect(reply, &QModbusReply::errorOccurred, this, [reply, requestId, this] (QModbusDevice::Error error){
qCWarning(dcModbusTcpMaster()) << "Modbus replay error for device" << m_hostAddress.toString() << ":" << error; qCWarning(dcModbusTcpMaster()) << "Modbus replay error for device" << connectionUrl() << ":" << error;
emit writeRequestError(requestId, reply->errorString()); emit writeRequestError(requestId, reply->errorString());
}); });
@ -240,7 +231,7 @@ QUuid ModbusTcpMaster::writeHoldingRegisters(uint slaveAddress, uint registerAdd
return QUuid(); return QUuid();
} }
} else { } else {
qCWarning(dcModbusTcpMaster()) << "Read error for device" << m_hostAddress.toString() << ":" << m_modbusTcpClient->errorString(); qCWarning(dcModbusTcpMaster()) << "Read error for device" << connectionUrl() << ":" << m_modbusTcpClient->errorString();
return QUuid(); return QUuid();
} }
return requestId; return requestId;
@ -268,9 +259,6 @@ QModbusReply *ModbusTcpMaster::sendWriteRequest(const QModbusDataUnit &write, in
QUuid ModbusTcpMaster::readDiscreteInput(uint slaveAddress, uint registerAddress, uint size) QUuid ModbusTcpMaster::readDiscreteInput(uint slaveAddress, uint registerAddress, uint size)
{ {
if (!m_modbusTcpClient) {
return QUuid();
}
QUuid requestId = QUuid::createUuid(); QUuid requestId = QUuid::createUuid();
QModbusDataUnit request = QModbusDataUnit(QModbusDataUnit::RegisterType::DiscreteInputs, registerAddress, size); QModbusDataUnit request = QModbusDataUnit(QModbusDataUnit::RegisterType::DiscreteInputs, registerAddress, size);
@ -286,12 +274,12 @@ QUuid ModbusTcpMaster::readDiscreteInput(uint slaveAddress, uint registerAddress
emit receivedDiscreteInput(reply->serverAddress(), modbusAddress, unit.values()); emit receivedDiscreteInput(reply->serverAddress(), modbusAddress, unit.values());
} else { } else {
emit readRequestExecuted(requestId, false); emit readRequestExecuted(requestId, false);
qCWarning(dcModbusTcpMaster()) << "Read response error for device" << m_hostAddress.toString() << ":" << reply->error(); qCWarning(dcModbusTcpMaster()) << "Read response error for device" << connectionUrl() << ":" << reply->error();
} }
}); });
connect(reply, &QModbusReply::errorOccurred, this, [requestId, reply, this] (QModbusDevice::Error error){ connect(reply, &QModbusReply::errorOccurred, this, [requestId, reply, this] (QModbusDevice::Error error){
qCWarning(dcModbusTcpMaster()) << "Modbus replay error for device" << m_hostAddress.toString() << ":" << error; qCWarning(dcModbusTcpMaster()) << "Modbus replay error for device" << connectionUrl() << ":" << error;
emit readRequestError(requestId, reply->errorString()); emit readRequestError(requestId, reply->errorString());
}); });
@ -301,7 +289,7 @@ QUuid ModbusTcpMaster::readDiscreteInput(uint slaveAddress, uint registerAddress
return QUuid(); return QUuid();
} }
} else { } else {
qCWarning(dcModbusTcpMaster()) << "Read error for device" << m_hostAddress.toString() << ":" << m_modbusTcpClient->errorString(); qCWarning(dcModbusTcpMaster()) << "Read error for device" << connectionUrl() << ":" << m_modbusTcpClient->errorString();
return QUuid(); return QUuid();
} }
return requestId; return requestId;
@ -328,12 +316,12 @@ QUuid ModbusTcpMaster::readInputRegister(uint slaveAddress, uint registerAddress
emit receivedInputRegister(reply->serverAddress(), modbusAddress, unit.values()); emit receivedInputRegister(reply->serverAddress(), modbusAddress, unit.values());
} else { } else {
emit readRequestExecuted(requestId, false); emit readRequestExecuted(requestId, false);
qCWarning(dcModbusTcpMaster()) << "Read response error for device" << m_hostAddress.toString() << ":" << reply->error(); qCWarning(dcModbusTcpMaster()) << "Read response error for device" << connectionUrl() << ":" << reply->error();
} }
}); });
connect(reply, &QModbusReply::errorOccurred, this, [reply, requestId, this] (QModbusDevice::Error error){ connect(reply, &QModbusReply::errorOccurred, this, [reply, requestId, this] (QModbusDevice::Error error){
qCWarning(dcModbusTcpMaster()) << "Modbus reply error for device" << m_hostAddress.toString() << ":" << error; qCWarning(dcModbusTcpMaster()) << "Modbus reply error for device" << connectionUrl() << ":" << error;
emit readRequestError(requestId, reply->errorString()); emit readRequestError(requestId, reply->errorString());
}); });
@ -344,7 +332,7 @@ QUuid ModbusTcpMaster::readInputRegister(uint slaveAddress, uint registerAddress
return QUuid(); return QUuid();
} }
} else { } else {
qCWarning(dcModbusTcpMaster()) << "Read error for device" << m_hostAddress.toString() << ":" << m_modbusTcpClient->errorString(); qCWarning(dcModbusTcpMaster()) << "Read error for device" << connectionUrl() << ":" << m_modbusTcpClient->errorString();
return QUuid(); return QUuid();
} }
return requestId; return requestId;
@ -352,10 +340,6 @@ QUuid ModbusTcpMaster::readInputRegister(uint slaveAddress, uint registerAddress
QUuid ModbusTcpMaster::readHoldingRegister(uint slaveAddress, uint registerAddress, uint size) QUuid ModbusTcpMaster::readHoldingRegister(uint slaveAddress, uint registerAddress, uint size)
{ {
if (!m_modbusTcpClient) {
return QUuid();
}
QUuid requestId = QUuid::createUuid(); QUuid requestId = QUuid::createUuid();
QModbusDataUnit request = QModbusDataUnit(QModbusDataUnit::RegisterType::HoldingRegisters, registerAddress, size); QModbusDataUnit request = QModbusDataUnit(QModbusDataUnit::RegisterType::HoldingRegisters, registerAddress, size);
@ -372,7 +356,7 @@ QUuid ModbusTcpMaster::readHoldingRegister(uint slaveAddress, uint registerAddre
} else { } else {
emit writeRequestExecuted(requestId, false); emit writeRequestExecuted(requestId, false);
qCWarning(dcModbusTcpMaster()) << "Read response error for device" << m_hostAddress.toString() << ":" << reply->error(); qCWarning(dcModbusTcpMaster()) << "Read response error for device" << connectionUrl() << ":" << reply->error();
emit readRequestError(requestId, reply->errorString()); emit readRequestError(requestId, reply->errorString());
} }
reply->deleteLater(); reply->deleteLater();
@ -380,7 +364,7 @@ QUuid ModbusTcpMaster::readHoldingRegister(uint slaveAddress, uint registerAddre
connect(reply, &QModbusReply::errorOccurred, this, [reply, requestId, this] (QModbusDevice::Error error){ connect(reply, &QModbusReply::errorOccurred, this, [reply, requestId, this] (QModbusDevice::Error error){
qCWarning(dcModbusTcpMaster()) << "Modbus reply error for device" << m_hostAddress.toString() << ":" << error; qCWarning(dcModbusTcpMaster()) << "Modbus reply error for device" << connectionUrl() << ":" << error;
emit readRequestError(requestId, reply->errorString()); emit readRequestError(requestId, reply->errorString());
}); });
@ -390,7 +374,7 @@ QUuid ModbusTcpMaster::readHoldingRegister(uint slaveAddress, uint registerAddre
return QUuid(); return QUuid();
} }
} else { } else {
qCWarning(dcModbusTcpMaster()) << "Read error for device" << m_hostAddress.toString() << ":" << m_modbusTcpClient->errorString(); qCWarning(dcModbusTcpMaster()) << "Read error for device" << connectionUrl() << ":" << m_modbusTcpClient->errorString();
return QUuid(); return QUuid();
} }
return requestId; return requestId;
@ -403,10 +387,6 @@ QUuid ModbusTcpMaster::writeCoil(uint slaveAddress, uint registerAddress, bool v
QUuid ModbusTcpMaster::writeCoils(uint slaveAddress, uint registerAddress, const QVector<quint16> &values) QUuid ModbusTcpMaster::writeCoils(uint slaveAddress, uint registerAddress, const QVector<quint16> &values)
{ {
if (!m_modbusTcpClient) {
return QUuid();
}
QUuid requestId = QUuid::createUuid(); QUuid requestId = QUuid::createUuid();
QModbusDataUnit request = QModbusDataUnit(QModbusDataUnit::RegisterType::Coils, registerAddress, values.length()); QModbusDataUnit request = QModbusDataUnit(QModbusDataUnit::RegisterType::Coils, registerAddress, values.length());
request.setValues(values); request.setValues(values);
@ -424,13 +404,13 @@ QUuid ModbusTcpMaster::writeCoils(uint slaveAddress, uint registerAddress, const
} else { } else {
emit writeRequestExecuted(requestId, false); emit writeRequestExecuted(requestId, false);
qCWarning(dcModbusTcpMaster()) << "Write response error for device" << m_hostAddress.toString() << ":" << reply->error(); qCWarning(dcModbusTcpMaster()) << "Write response error for device" << connectionUrl() << ":" << reply->error();
} }
reply->deleteLater(); reply->deleteLater();
}); });
connect(reply, &QModbusReply::errorOccurred, this, [reply, requestId, this] (QModbusDevice::Error error){ connect(reply, &QModbusReply::errorOccurred, this, [reply, requestId, this] (QModbusDevice::Error error){
qCWarning(dcModbusTcpMaster()) << "Modbus reply error for device" << m_hostAddress.toString() << ":" << error; qCWarning(dcModbusTcpMaster()) << "Modbus reply error for device" << connectionUrl() << ":" << error;
emit writeRequestError(requestId, reply->errorString()); emit writeRequestError(requestId, reply->errorString());
}); });
@ -440,7 +420,7 @@ QUuid ModbusTcpMaster::writeCoils(uint slaveAddress, uint registerAddress, const
return QUuid(); return QUuid();
} }
} else { } else {
qCWarning(dcModbusTcpMaster()) << "Read error for device" << m_hostAddress.toString() << ":" << m_modbusTcpClient->errorString(); qCWarning(dcModbusTcpMaster()) << "Read error for device" << connectionUrl() << ":" << m_modbusTcpClient->errorString();
return QUuid(); return QUuid();
} }
return requestId; return requestId;
@ -453,7 +433,7 @@ QUuid ModbusTcpMaster::writeHoldingRegister(uint slaveAddress, uint registerAddr
void ModbusTcpMaster::onModbusErrorOccurred(QModbusDevice::Error error) void ModbusTcpMaster::onModbusErrorOccurred(QModbusDevice::Error error)
{ {
qCWarning(dcModbusTcpMaster()) << "An error occurred for device" << m_hostAddress.toString() << ":" << error; qCWarning(dcModbusTcpMaster()) << "An error occurred for device" << connectionUrl() << ":" << error;
emit connectionErrorOccurred(error); emit connectionErrorOccurred(error);
} }

View File

@ -55,6 +55,8 @@ public:
uint port() const; uint port() const;
void setPort(uint port); void setPort(uint port);
QString connectionUrl() const;
bool connected() const; bool connected() const;
int numberOfRetries() const; int numberOfRetries() const;

View File

@ -335,7 +335,7 @@ def writeInitMethodImplementationTcp(fileDescriptor, className, registerDefiniti
# First check if there are any init registers # First check if there are any init registers
initRequired = False initRequired = False
for registerDefinition in registerDefinitions: for registerDefinition in registerDefinitions:
if registerDefinition['readSchedule'] == 'init': if 'readSchedule' in registerDefinition and registerDefinition['readSchedule'] == 'init':
initRequired = True initRequired = True
break break

View File

@ -1,17 +1,21 @@
# Webasto # Webasto
## Supported Things Connects nymea to Webasto wallboxes. Currently supported models:
* AC Wallbox Live * Webasto Live
* Webasto NEXT
## Requirements ## Requirements
* The packages 'nymea-plugin-webasto' must be installed. nymea uses the Modbus TCP connection to connect to the wallbox.
* The modbus server must be enabled
* The setting 'Modbus Slave Register Address Set' must be set to 'TQ-DM100' The modbus server must be enabled on the Wallbox.
* The setting 'Modbus TCP Server Port Number' must be set to 502
For Webasto NEXT this can be done by enabling the `Home energy management` using the Webasto App.
For the Webasto Live the setting `Modbus Slave Register Address Set` must be set to `TQ-DM100`.
## More ## More
https://dealers.webasto.com/Sections/Public/Documents.aspx?SectionId=6&CategoryId=9&ProductTypeId=66&ProductId=630&ShowResult=true https://charging.webasto.com/

View File

@ -1,6 +1,6 @@
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
* *
* Copyright 2013 - 2020, nymea GmbH * Copyright 2013 - 2023, nymea GmbH
* Contact: contact@nymea.io * Contact: contact@nymea.io
* *
* This file is part of nymea. * This file is part of nymea.
@ -29,10 +29,12 @@
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
#include "integrationpluginwebasto.h" #include "integrationpluginwebasto.h"
#include "webastodiscovery.h"
#include "plugininfo.h" #include "plugininfo.h"
#include <network/networkdevicediscovery.h>
#include <types/param.h> #include <types/param.h>
#include <hardware/electricity.h>
#include <network/networkdevicediscovery.h>
#include <QDebug> #include <QDebug>
#include <QStringList> #include <QStringList>
@ -51,14 +53,14 @@ void IntegrationPluginWebasto::init()
void IntegrationPluginWebasto::discoverThings(ThingDiscoveryInfo *info) void IntegrationPluginWebasto::discoverThings(ThingDiscoveryInfo *info)
{ {
if (info->thingClassId() == webastoLiveThingClassId) { if (!hardwareManager()->networkDeviceDiscovery()->available()) {
if (!hardwareManager()->networkDeviceDiscovery()->available()) { qCWarning(dcWebasto()) << "Failed to discover network devices. The network device discovery is not available.";
qCWarning(dcWebasto()) << "Failed to discover network devices. The network device discovery is not available."; info->finish(Thing::ThingErrorHardwareNotAvailable, QT_TR_NOOP("The discovery is not available."));
info->finish(Thing::ThingErrorHardwareNotAvailable, QT_TR_NOOP("The discovery is not available.")); return;
return; }
}
qCDebug(dcWebasto()) << "Discover things"; if (info->thingClassId() == webastoLiveThingClassId) {
qCInfo(dcWebasto()) << "Start discovering webasto live in the local network...";
NetworkDeviceDiscoveryReply *discoveryReply = hardwareManager()->networkDeviceDiscovery()->discover(); NetworkDeviceDiscoveryReply *discoveryReply = hardwareManager()->networkDeviceDiscovery()->discover();
connect(discoveryReply, &NetworkDeviceDiscoveryReply::finished, discoveryReply, &NetworkDeviceDiscoveryReply::deleteLater); connect(discoveryReply, &NetworkDeviceDiscoveryReply::finished, discoveryReply, &NetworkDeviceDiscoveryReply::deleteLater);
connect(discoveryReply, &NetworkDeviceDiscoveryReply::finished, this, [=](){ connect(discoveryReply, &NetworkDeviceDiscoveryReply::finished, this, [=](){
@ -69,7 +71,7 @@ void IntegrationPluginWebasto::discoverThings(ThingDiscoveryInfo *info)
if (!networkDeviceInfo.hostName().contains("webasto", Qt::CaseSensitivity::CaseInsensitive)) if (!networkDeviceInfo.hostName().contains("webasto", Qt::CaseSensitivity::CaseInsensitive))
continue; continue;
QString title = "Wallbox "; QString title = "Webasto Live";
if (networkDeviceInfo.hostName().isEmpty()) { if (networkDeviceInfo.hostName().isEmpty()) {
title += networkDeviceInfo.address().toString(); title += networkDeviceInfo.address().toString();
} else { } else {
@ -100,9 +102,55 @@ void IntegrationPluginWebasto::discoverThings(ThingDiscoveryInfo *info)
} }
info->finish(Thing::ThingErrorNoError); info->finish(Thing::ThingErrorNoError);
}); });
} else {
Q_ASSERT_X(false, "discoverThings", QString("Unhandled thingClassId: %1").arg(info->thingClassId().toString()).toUtf8()); return;
} }
if (info->thingClassId() == webastoNextThingClassId) {
qCInfo(dcWebasto()) << "Start discovering Webasto NEXT in the local network...";
// Create a discovery with the info as parent for auto deleting the object once the discovery info is done
WebastoDiscovery *discovery = new WebastoDiscovery(hardwareManager()->networkDeviceDiscovery(), info);
connect(discovery, &WebastoDiscovery::discoveryFinished, info, [=](){
foreach (const WebastoDiscovery::Result &result, discovery->results()) {
QString title = "Webasto Next";
if (!result.networkDeviceInfo.hostName().isEmpty()){
title.append(" (" + result.networkDeviceInfo.hostName() + ")");
}
QString description = result.networkDeviceInfo.address().toString();
if (result.networkDeviceInfo.macAddressManufacturer().isEmpty()) {
description += " " + result.networkDeviceInfo.macAddress();
} else {
description += " " + result.networkDeviceInfo.macAddress() + " (" + result.networkDeviceInfo.macAddressManufacturer() + ")";
}
ThingDescriptor descriptor(webastoNextThingClassId, title, description);
// Check if we already have set up this device
Things existingThings = myThings().filterByParam(webastoNextThingMacAddressParamTypeId, result.networkDeviceInfo.macAddress());
if (existingThings.count() == 1) {
qCDebug(dcWebasto()) << "This thing already exists in the system." << existingThings.first() << result.networkDeviceInfo;
descriptor.setThingId(existingThings.first()->id());
}
ParamList params;
params << Param(webastoNextThingMacAddressParamTypeId, result.networkDeviceInfo.macAddress());
descriptor.setParams(params);
info->addThingDescriptor(descriptor);
}
info->finish(Thing::ThingErrorNoError);
});
discovery->startDiscovery();
return;
}
Q_ASSERT_X(false, "discoverThings", QString("Unhandled thingClassId: %1").arg(info->thingClassId().toString()).toUtf8());
} }
void IntegrationPluginWebasto::setupThing(ThingSetupInfo *info) void IntegrationPluginWebasto::setupThing(ThingSetupInfo *info)
@ -112,14 +160,15 @@ void IntegrationPluginWebasto::setupThing(ThingSetupInfo *info)
if (thing->thingClassId() == webastoLiveThingClassId) { if (thing->thingClassId() == webastoLiveThingClassId) {
if (m_webastoConnections.contains(thing)) { if (m_webastoLiveConnections.contains(thing)) {
// Clean up after reconfiguration // Clean up after reconfiguration
m_webastoConnections.take(thing)->deleteLater(); m_webastoLiveConnections.take(thing)->deleteLater();
} }
QHostAddress address = QHostAddress(thing->paramValue(webastoLiveThingIpAddressParamTypeId).toString()); QHostAddress address = QHostAddress(thing->paramValue(webastoLiveThingIpAddressParamTypeId).toString());
Webasto *webasto = new Webasto(address, 502, thing); Webasto *webasto = new Webasto(address, 502, thing);
m_webastoConnections.insert(thing, webasto); m_webastoLiveConnections.insert(thing, webasto);
connect(webasto, &Webasto::destroyed, this, [thing, this] {m_webastoConnections.remove(thing);}); connect(webasto, &Webasto::destroyed, this, [thing, this] {m_webastoLiveConnections.remove(thing);});
connect(webasto, &Webasto::connectionStateChanged, this, &IntegrationPluginWebasto::onConnectionChanged); connect(webasto, &Webasto::connectionStateChanged, this, &IntegrationPluginWebasto::onConnectionChanged);
connect(webasto, &Webasto::receivedRegister, this, &IntegrationPluginWebasto::onReceivedRegister); connect(webasto, &Webasto::receivedRegister, this, &IntegrationPluginWebasto::onReceivedRegister);
connect(webasto, &Webasto::writeRequestError, this, &IntegrationPluginWebasto::onWriteRequestError); connect(webasto, &Webasto::writeRequestError, this, &IntegrationPluginWebasto::onWriteRequestError);
@ -132,36 +181,142 @@ void IntegrationPluginWebasto::setupThing(ThingSetupInfo *info)
if (connected) if (connected)
info->finish(Thing::ThingErrorNoError); info->finish(Thing::ThingErrorNoError);
}); });
} else {
Q_ASSERT_X(false, "setupThing", QString("Unhandled thingClassId: %1").arg(thing->thingClassId().toString()).toUtf8()); return;
} }
if (thing->thingClassId() == webastoNextThingClassId) {
// Handle reconfigure
if (m_webastoNextConnections.contains(thing)) {
qCDebug(dcWebasto()) << "Reconfiguring existing thing" << thing->name();
m_webastoNextConnections.take(thing)->deleteLater();
if (m_monitors.contains(thing)) {
hardwareManager()->networkDeviceDiscovery()->unregisterMonitor(m_monitors.take(thing));
}
}
MacAddress macAddress = MacAddress(thing->paramValue(webastoNextThingMacAddressParamTypeId).toString());
if (!macAddress.isValid()) {
qCWarning(dcWebasto()) << "The configured mac address is not valid" << thing->params();
info->finish(Thing::ThingErrorInvalidParameter, QT_TR_NOOP("The MAC address is not known. Please reconfigure the thing."));
return;
}
// Create the monitor
NetworkDeviceMonitor *monitor = hardwareManager()->networkDeviceDiscovery()->registerMonitor(macAddress);
m_monitors.insert(thing, monitor);
QHostAddress address = monitor->networkDeviceInfo().address();
if (address.isNull()) {
qCWarning(dcWebasto()) << "Cannot set up thing. The host address is not known yet. Maybe it will be available in the next run...";
hardwareManager()->networkDeviceDiscovery()->unregisterMonitor(m_monitors.take(thing));
info->finish(Thing::ThingErrorHardwareFailure, QT_TR_NOOP("The host address is not known yet. Trying later again."));
return;
}
// Clean up in case the setup gets aborted
connect(info, &ThingSetupInfo::aborted, monitor, [=](){
if (m_monitors.contains(thing)) {
qCDebug(dcWebasto()) << "Unregister monitor because setup has been aborted.";
hardwareManager()->networkDeviceDiscovery()->unregisterMonitor(m_monitors.take(thing));
}
});
// If this is the first setup, the monitor must become reachable before we finish the setup
if (info->isInitialSetup()) {
// Wait for the monitor to be ready
if (monitor->reachable()) {
// Thing already reachable...let's continue with the setup
setupWebastoNextConnection(info);
} else {
qCDebug(dcWebasto()) << "Waiting for the network monitor to get reachable before continue to set up the connection" << thing->name() << address.toString() << "...";
connect(monitor, &NetworkDeviceMonitor::reachableChanged, info, [=](bool reachable){
if (reachable) {
qCDebug(dcWebasto()) << "The monitor for thing setup" << thing->name() << "is now reachable. Continue setup...";
setupWebastoNextConnection(info);
}
});
}
} else {
// Not the first setup, just add and let the monitor do the check reachable work
setupWebastoNextConnection(info);
}
return;
}
Q_ASSERT_X(false, "setupThing", QString("Unhandled thingClassId: %1").arg(thing->thingClassId().toString()).toUtf8());
} }
void IntegrationPluginWebasto::postSetupThing(Thing *thing) void IntegrationPluginWebasto::postSetupThing(Thing *thing)
{ {
qCDebug(dcWebasto()) << "Post setup thing" << thing->name(); qCDebug(dcWebasto()) << "Post setup thing" << thing->name();
if (!m_pluginTimer) { if (!m_pluginTimer) {
qCDebug(dcWebasto()) << "Setting up refresh timer for Webasto connections";
m_pluginTimer = hardwareManager()->pluginTimerManager()->registerTimer(1); m_pluginTimer = hardwareManager()->pluginTimerManager()->registerTimer(1);
connect(m_pluginTimer, &PluginTimer::timeout, this, [this] { connect(m_pluginTimer, &PluginTimer::timeout, this, [this] {
Q_FOREACH(Webasto *connection, m_webastoConnections) {
if (connection->connected()) foreach(Webasto *connection, m_webastoLiveConnections) {
if (connection->connected()) {
update(connection); update(connection);
}
}
foreach(WebastoNextModbusTcpConnection *webastoNext, m_webastoNextConnections) {
if (webastoNext->reachable()) {
webastoNext->update();
}
} }
}); });
m_pluginTimer->start();
} }
if (thing->thingClassId() == webastoLiveThingClassId) { if (thing->thingClassId() == webastoLiveThingClassId) {
Webasto *connection = m_webastoConnections.value(thing); Webasto *connection = m_webastoLiveConnections.value(thing);
if (!connection) {
qCWarning(dcWebasto()) << "Can't find connection to thing";
}
update(connection); update(connection);
return;
}
} else { if (thing->thingClassId() == webastoNextThingClassId) {
Q_ASSERT_X(false, "postSetupThing", QString("Unhandled thingClassId: %1").arg(thing->thingClassId().toString()).toUtf8()); WebastoNextModbusTcpConnection *connection = m_webastoNextConnections.value(thing);
if (connection->reachable()) {
thing->setStateValue(webastoNextConnectedStateTypeId, true);
connection->update();
} else {
// We start the connection mechanism only if the monitor says the thing is reachable
if (m_monitors.value(thing)->reachable()) {
connection->connectDevice();
}
}
return;
} }
} }
void IntegrationPluginWebasto::thingRemoved(Thing *thing)
{
qCDebug(dcWebasto()) << "Delete thing" << thing->name();
if (thing->thingClassId() == webastoNextThingClassId) {
WebastoNextModbusTcpConnection *connection = m_webastoNextConnections.take(thing);
connection->disconnectDevice();
connection->deleteLater();
}
// Unregister related hardware resources
if (m_monitors.contains(thing))
hardwareManager()->networkDeviceDiscovery()->unregisterMonitor(m_monitors.take(thing));
if (m_pluginTimer && myThings().isEmpty()) {
hardwareManager()->pluginTimerManager()->unregisterTimer(m_pluginTimer);
m_pluginTimer = nullptr;
}
}
void IntegrationPluginWebasto::executeAction(ThingActionInfo *info) void IntegrationPluginWebasto::executeAction(ThingActionInfo *info)
{ {
Thing *thing = info->thing(); Thing *thing = info->thing();
@ -169,7 +324,7 @@ void IntegrationPluginWebasto::executeAction(ThingActionInfo *info)
if (thing->thingClassId() == webastoLiveThingClassId) { if (thing->thingClassId() == webastoLiveThingClassId) {
Webasto *connection = m_webastoConnections.value(thing); Webasto *connection = m_webastoLiveConnections.value(thing);
if (!connection) { if (!connection) {
qCWarning(dcWebasto()) << "Can't find connection to thing"; qCWarning(dcWebasto()) << "Can't find connection to thing";
return info->finish(Thing::ThingErrorHardwareNotAvailable); return info->finish(Thing::ThingErrorHardwareNotAvailable);
@ -200,20 +355,338 @@ void IntegrationPluginWebasto::executeAction(ThingActionInfo *info)
} else { } else {
Q_ASSERT_X(false, "executeAction", QString("Unhandled actionTypeId: %1").arg(action.actionTypeId().toString()).toUtf8()); Q_ASSERT_X(false, "executeAction", QString("Unhandled actionTypeId: %1").arg(action.actionTypeId().toString()).toUtf8());
} }
} else {
Q_ASSERT_X(false, "executeAction", QString("Unhandled thingClassId: %1").arg(thing->thingClassId().toString()).toUtf8()); return;
} }
if (thing->thingClassId() == webastoNextThingClassId) {
WebastoNextModbusTcpConnection *connection = m_webastoNextConnections.value(thing);
if (!connection) {
qCWarning(dcWebasto()) << "Can't find modbus connection for" << thing;
info->finish(Thing::ThingErrorHardwareNotAvailable);
return;
}
if (!connection->reachable()) {
qCWarning(dcWebasto()) << "Cannot execute action because the connection of" << thing << "is not reachable.";
info->finish(Thing::ThingErrorHardwareNotAvailable, QT_TR_NOOP("The charging station is not reachable."));
return;
}
if (action.actionTypeId() == webastoNextPowerActionTypeId) {
bool power = action.paramValue(webastoNextPowerActionPowerParamTypeId).toBool();
// If this action was executed by the user, we start a new session, otherwise we assume it was a some charging logic
// and we keep the current session.
if (power && action.triggeredBy() == Action::TriggeredByUser) {
// First send 0 ChargingActionNoAction before sending 1 start session
qCDebug(dcWebasto()) << "Enable charging action triggered by user. Restarting the session.";
QModbusReply *reply = connection->setChargingAction(WebastoNextModbusTcpConnection::ChargingActionNoAction);
connect(reply, &QModbusReply::finished, reply, &QModbusReply::deleteLater);
connect(reply, &QModbusReply::finished, info, [this, info, reply, power](){
if (reply->error() == QModbusDevice::NoError) {
info->thing()->setStateValue(webastoNextPowerStateTypeId, power);
qCDebug(dcWebasto()) << "Restart charging session request finished successfully.";
info->finish(Thing::ThingErrorNoError);
} else {
qCWarning(dcWebasto()) << "Restart charging session request finished with error:" << reply->errorString();
info->finish(Thing::ThingErrorHardwareFailure);
}
// Note: even if "NoAction" failed, we try to send the start charging action and report the error there just in case
executeWebastoNextPowerAction(info, power);
});
} else {
executeWebastoNextPowerAction(info, power);
}
} else if (action.actionTypeId() == webastoNextMaxChargingCurrentActionTypeId) {
quint16 chargingCurrent = action.paramValue(webastoNextMaxChargingCurrentActionMaxChargingCurrentParamTypeId).toUInt();
qCDebug(dcWebasto()) << "Set max charging current of" << thing << "to" << chargingCurrent << "ampere";
QModbusReply *reply = connection->setChargeCurrent(chargingCurrent);
connect(reply, &QModbusReply::finished, reply, &QModbusReply::deleteLater);
connect(reply, &QModbusReply::finished, info, [info, reply, chargingCurrent](){
if (reply->error() == QModbusDevice::NoError) {
qCDebug(dcWebasto()) << "Set max charging current finished successfully.";
info->thing()->setStateValue(webastoNextMaxChargingCurrentStateTypeId, chargingCurrent);
info->finish(Thing::ThingErrorNoError);
} else {
qCWarning(dcWebasto()) << "Set max charging current request finished with error:" << reply->errorString();
info->finish(Thing::ThingErrorHardwareFailure);
}
});
} else {
Q_ASSERT_X(false, "executeAction", QString("Unhandled actionTypeId: %1").arg(action.actionTypeId().toString()).toUtf8());
}
return;
}
Q_ASSERT_X(false, "executeAction", QString("Unhandled thingClassId: %1").arg(thing->thingClassId().toString()).toUtf8());
} }
void IntegrationPluginWebasto::thingRemoved(Thing *thing)
{
qCDebug(dcWebasto()) << "Delete thing" << thing->name();
if (thing->thingClassId() == webastoLiveThingClassId) {
}
if (myThings().isEmpty()) { void IntegrationPluginWebasto::setupWebastoNextConnection(ThingSetupInfo *info)
//Stop timer {
} Thing *thing = info->thing();
QHostAddress address = m_monitors.value(thing)->networkDeviceInfo().address();
uint port = thing->paramValue(webastoNextThingPortParamTypeId).toUInt();
quint16 slaveId = thing->paramValue(webastoNextThingSlaveIdParamTypeId).toUInt();
qCDebug(dcWebasto()) << "Setting up webasto next connection on" << QString("%1:%2").arg(address.toString()).arg(port) << "slave ID:" << slaveId;
WebastoNextModbusTcpConnection *webastoNextConnection = new WebastoNextModbusTcpConnection(address, port, slaveId, this);
webastoNextConnection->modbusTcpMaster()->setTimeout(500);
webastoNextConnection->modbusTcpMaster()->setNumberOfRetries(3);
m_webastoNextConnections.insert(thing, webastoNextConnection);
connect(info, &ThingSetupInfo::aborted, webastoNextConnection, [=](){
webastoNextConnection->deleteLater();
m_webastoNextConnections.remove(thing);
});
// Reconnect on monitor reachable changed
NetworkDeviceMonitor *monitor = m_monitors.value(thing);
connect(monitor, &NetworkDeviceMonitor::reachableChanged, thing, [=](bool reachable){
if (reachable) {
qCDebug(dcWebasto()) << "Network device is now reachable for" << thing << monitor->networkDeviceInfo();
} else {
qCDebug(dcWebasto()) << "Network device not reachable any more" << thing;
}
if (!thing->setupComplete())
return;
if (reachable) {
webastoNextConnection->modbusTcpMaster()->setHostAddress(monitor->networkDeviceInfo().address());
webastoNextConnection->reconnectDevice();
} else {
// Note: We disable autoreconnect explicitly and we will
// connect the device once the monitor says it is reachable again
webastoNextConnection->disconnectDevice();
}
});
connect(webastoNextConnection, &WebastoNextModbusTcpConnection::reachableChanged, thing, [thing, webastoNextConnection, monitor](bool reachable){
qCDebug(dcWebasto()) << "Reachable changed to" << reachable << "for" << thing;
thing->setStateValue(webastoNextConnectedStateTypeId, reachable);
if (reachable) {
// Connected true will be set after successfull init
webastoNextConnection->update();
} else {
thing->setStateValue(webastoNextCurrentPowerStateTypeId, 0);
thing->setStateValue(webastoNextCurrentPowerPhaseAStateTypeId, 0);
thing->setStateValue(webastoNextCurrentPowerPhaseBStateTypeId, 0);
thing->setStateValue(webastoNextCurrentPowerPhaseCStateTypeId, 0);
thing->setStateValue(webastoNextCurrentPhaseAStateTypeId, 0);
thing->setStateValue(webastoNextCurrentPhaseBStateTypeId, 0);
thing->setStateValue(webastoNextCurrentPhaseCStateTypeId, 0);
if (monitor->reachable()) {
webastoNextConnection->reconnectDevice();
}
}
});
connect(webastoNextConnection, &WebastoNextModbusTcpConnection::updateFinished, thing, [thing, webastoNextConnection](){
// Note: we get the update finished also if all calles failed...
if (!webastoNextConnection->reachable()) {
thing->setStateValue(webastoNextConnectedStateTypeId, false);
return;
}
thing->setStateValue(webastoNextConnectedStateTypeId, true);
qCDebug(dcWebasto()) << "Update finished" << webastoNextConnection;
// States
switch (webastoNextConnection->chargeState()) {
case WebastoNextModbusTcpConnection::ChargeStateIdle:
thing->setStateValue(webastoNextChargingStateTypeId, false);
break;
case WebastoNextModbusTcpConnection::ChargeStateCharging:
thing->setStateValue(webastoNextChargingStateTypeId, true);
break;
}
switch (webastoNextConnection->chargerState()) {
case WebastoNextModbusTcpConnection::ChargerStateNoVehicle:
thing->setStateValue(webastoNextChargingStateTypeId, false);
thing->setStateValue(webastoNextPluggedInStateTypeId, false);
break;
case WebastoNextModbusTcpConnection::ChargerStateVehicleAttachedNoPermission:
thing->setStateValue(webastoNextPluggedInStateTypeId, true);
break;
case WebastoNextModbusTcpConnection::ChargerStateCharging:
thing->setStateValue(webastoNextChargingStateTypeId, true);
thing->setStateValue(webastoNextPluggedInStateTypeId, true);
break;
case WebastoNextModbusTcpConnection::ChargerStateChargingPaused:
thing->setStateValue(webastoNextPluggedInStateTypeId, true);
break;
default:
break;
}
// Meter values
thing->setStateValue(webastoNextCurrentPowerPhaseAStateTypeId, webastoNextConnection->activePowerL1());
thing->setStateValue(webastoNextCurrentPowerPhaseBStateTypeId, webastoNextConnection->activePowerL2());
thing->setStateValue(webastoNextCurrentPowerPhaseCStateTypeId, webastoNextConnection->activePowerL3());
double currentPhaseA = webastoNextConnection->currentL1() / 1000.0;
double currentPhaseB = webastoNextConnection->currentL2() / 1000.0;
double currentPhaseC = webastoNextConnection->currentL3() / 1000.0;
thing->setStateValue(webastoNextCurrentPhaseAStateTypeId, currentPhaseA);
thing->setStateValue(webastoNextCurrentPhaseBStateTypeId, currentPhaseB);
thing->setStateValue(webastoNextCurrentPhaseCStateTypeId, currentPhaseC);
// Note: we do not use the active phase power, because we have sometimes a few watts on inactive phases
Electricity::Phases phases = Electricity::PhaseNone;
phases.setFlag(Electricity::PhaseA, currentPhaseA > 0);
phases.setFlag(Electricity::PhaseB, currentPhaseB > 0);
phases.setFlag(Electricity::PhaseC, currentPhaseC > 0);
if (phases != Electricity::PhaseNone) {
thing->setStateValue(webastoNextUsedPhasesStateTypeId, Electricity::convertPhasesToString(phases));
thing->setStateValue(webastoNextPhaseCountStateTypeId, Electricity::getPhaseCount(phases));
}
thing->setStateValue(webastoNextCurrentPowerStateTypeId, webastoNextConnection->totalActivePower());
thing->setStateValue(webastoNextTotalEnergyConsumedStateTypeId, webastoNextConnection->energyConsumed() / 1000.0);
thing->setStateValue(webastoNextSessionEnergyStateTypeId, webastoNextConnection->sessionEnergy() / 1000.0);
// Min / Max charging current^
thing->setStateValue(webastoNextMinCurrentTotalStateTypeId, webastoNextConnection->minChargingCurrent());
thing->setStateValue(webastoNextMaxCurrentTotalStateTypeId, webastoNextConnection->maxChargingCurrent());
thing->setStateMinValue(webastoNextMaxChargingCurrentStateTypeId, webastoNextConnection->minChargingCurrent());
thing->setStateMaxValue(webastoNextMaxChargingCurrentStateTypeId, webastoNextConnection->maxChargingCurrent());
thing->setStateValue(webastoNextMaxCurrentChargerStateTypeId, webastoNextConnection->maxChargingCurrentStation());
thing->setStateValue(webastoNextMaxCurrentCableStateTypeId, webastoNextConnection->maxChargingCurrentCable());
thing->setStateValue(webastoNextMaxCurrentElectricVehicleStateTypeId, webastoNextConnection->maxChargingCurrentEv());
if (webastoNextConnection->evseErrorCode() == 0) {
thing->setStateValue(webastoNextErrorStateTypeId, "");
} else {
uint errorCode = webastoNextConnection->evseErrorCode() - 1;
switch (errorCode) {
case 1:
// Note: also PB61 has the same mapping and the same reason for the error.
// We inform only about the PB02 since it does not make any difference regarding the action
thing->setStateValue(webastoNextErrorStateTypeId, "PB02 - PowerSwitch Failure");
break;
case 2:
thing->setStateValue(webastoNextErrorStateTypeId, "PB07 - InternalError (Aux Voltage)");
break;
case 3:
thing->setStateValue(webastoNextErrorStateTypeId, "PB09 - EV Communication Error");
break;
case 4:
thing->setStateValue(webastoNextErrorStateTypeId, "PB17 - OverVoltage");
break;
case 5:
thing->setStateValue(webastoNextErrorStateTypeId, "PB18 - UnderVoltage");
break;
case 6:
thing->setStateValue(webastoNextErrorStateTypeId, "PB23 - OverCurrent Failure");
break;
case 7:
thing->setStateValue(webastoNextErrorStateTypeId, "PB24 - OtherError");
break;
case 8:
thing->setStateValue(webastoNextErrorStateTypeId, "PB27 - GroundFailure");
break;
case 9:
thing->setStateValue(webastoNextErrorStateTypeId, "PB28 - InternalError (Selftest)");
break;
case 10:
thing->setStateValue(webastoNextErrorStateTypeId, "PB29 - High Temperature");
break;
case 11:
thing->setStateValue(webastoNextErrorStateTypeId, "PB52 - Proximity Pilot Error");
break;
case 12:
thing->setStateValue(webastoNextErrorStateTypeId, "PB53 - Shutter Error");
break;
case 13:
thing->setStateValue(webastoNextErrorStateTypeId, "PB57 - Error Three Phase Check");
break;
case 14:
thing->setStateValue(webastoNextErrorStateTypeId, "PB59 - PWR internal error");
break;
case 15:
thing->setStateValue(webastoNextErrorStateTypeId, "PB60 - EV Communication Error - Negative control pilot voltage");
break;
case 16:
thing->setStateValue(webastoNextErrorStateTypeId, "PB62- DC residual current (Vehicle)");
break;
default:
thing->setStateValue(webastoNextErrorStateTypeId, QString("Unknwon error code %1").arg(errorCode));
break;
}
}
// Handle life bit (keep alive mechanism if there is a HEMS activated)
if (webastoNextConnection->lifeBit() == 0) {
// Let's reset the life bit so the wallbox knows we are still here,
// otherwise the wallbox goes into the failsave mode and limits the charging to the configured
QModbusReply *reply = webastoNextConnection->setLifeBit(1);
connect(reply, &QModbusReply::finished, reply, &QModbusReply::deleteLater);
connect(reply, &QModbusReply::finished, webastoNextConnection, [reply, webastoNextConnection](){
if (reply->error() == QModbusDevice::NoError) {
qCDebug(dcWebasto()) << "Resetted life bit watchdog on" << webastoNextConnection << "finished successfully";
} else {
qCWarning(dcWebasto()) << "Resetted life bit watchdog on" << webastoNextConnection << "finished with error:" << reply->errorString();
}
});
}
});
connect(thing, &Thing::settingChanged, webastoNextConnection, [webastoNextConnection](const ParamTypeId &paramTypeId, const QVariant &value){
if (paramTypeId == webastoNextSettingsCommunicationTimeoutParamTypeId) {
QModbusReply *reply = webastoNextConnection->setComTimeout(value.toUInt());
connect(reply, &QModbusReply::finished, reply, &QModbusReply::deleteLater);
connect(reply, &QModbusReply::finished, webastoNextConnection, [reply, webastoNextConnection, value](){
if (reply->error() == QModbusDevice::NoError) {
qCDebug(dcWebasto()) << "Setting communication timout to" << value.toUInt() << "on" << webastoNextConnection << "finished successfully.";
} else {
qCWarning(dcWebasto()) << "Setting communication timout to" << value.toUInt() << "on" << webastoNextConnection << "finished with error:" << reply->errorString();
if (webastoNextConnection->reachable()) {
webastoNextConnection->updateComTimeout();
}
}
});
} else if (paramTypeId == webastoNextSettingsSafeCurrentParamTypeId) {
QModbusReply *reply = webastoNextConnection->setSafeCurrent(value.toUInt());
connect(reply, &QModbusReply::finished, reply, &QModbusReply::deleteLater);
connect(reply, &QModbusReply::finished, webastoNextConnection, [reply, webastoNextConnection, value](){
if (reply->error() == QModbusDevice::NoError) {
qCDebug(dcWebasto()) << "Setting save current to" << value.toUInt() << "on" << webastoNextConnection << "finished successfully.";
} else {
qCWarning(dcWebasto()) << "Setting save current to" << value.toUInt() << "on" << webastoNextConnection << "finished with error:" << reply->errorString();
if (webastoNextConnection->reachable()) {
webastoNextConnection->updateSafeCurrent();
}
}
});
} else {
qCWarning(dcWebasto()) << "Unhandled setting changed for" << webastoNextConnection;
}
});
connect(webastoNextConnection, &WebastoNextModbusTcpConnection::comTimeoutChanged, thing, [thing](quint16 comTimeout){
thing->setSettingValue(webastoNextSettingsCommunicationTimeoutParamTypeId, comTimeout);
});
connect(webastoNextConnection, &WebastoNextModbusTcpConnection::safeCurrentChanged, thing, [thing](quint16 safeCurrent){
thing->setSettingValue(webastoNextSettingsSafeCurrentParamTypeId, safeCurrent);
});
qCInfo(dcWebasto()) << "Setup finished successfully for Webasto NEXT" << thing << monitor;
info->finish(Thing::ThingErrorNoError);
} }
void IntegrationPluginWebasto::update(Webasto *webasto) void IntegrationPluginWebasto::update(Webasto *webasto)
@ -258,10 +731,35 @@ void IntegrationPluginWebasto::evaluatePhaseCount(Thing *thing)
} }
} }
void IntegrationPluginWebasto::executeWebastoNextPowerAction(ThingActionInfo *info, bool power)
{
qCDebug(dcWebasto()) << (power ? "Enabling": "Disabling") << "charging on" << info->thing();
WebastoNextModbusTcpConnection *connection = m_webastoNextConnections.value(info->thing());
QModbusReply *reply = nullptr;
if (power) {
reply = connection->setChargingAction(WebastoNextModbusTcpConnection::ChargingActionStartSession);
} else {
reply = connection->setChargingAction(WebastoNextModbusTcpConnection::ChargingActionCancelSession);
}
connect(reply, &QModbusReply::finished, reply, &QModbusReply::deleteLater);
connect(reply, &QModbusReply::finished, info, [info, reply, power](){
if (reply->error() == QModbusDevice::NoError) {
info->thing()->setStateValue(webastoNextPowerStateTypeId, power);
qCDebug(dcWebasto()) << "Enabling/disabling charging request finished successfully.";
info->finish(Thing::ThingErrorNoError);
} else {
qCWarning(dcWebasto()) << "Enabling/disabling charging request finished with error:" << reply->errorString();
info->finish(Thing::ThingErrorHardwareFailure);
}
});
}
void IntegrationPluginWebasto::onConnectionChanged(bool connected) void IntegrationPluginWebasto::onConnectionChanged(bool connected)
{ {
Webasto *connection = static_cast<Webasto *>(sender()); Webasto *connection = static_cast<Webasto *>(sender());
Thing *thing = m_webastoConnections.key(connection); Thing *thing = m_webastoLiveConnections.key(connection);
if (!thing) { if (!thing) {
qCWarning(dcWebasto()) << "On connection changed, thing not found for connection"; qCWarning(dcWebasto()) << "On connection changed, thing not found for connection";
return; return;
@ -290,7 +788,7 @@ void IntegrationPluginWebasto::onWriteRequestError(const QUuid &requestId, const
void IntegrationPluginWebasto::onReceivedRegister(Webasto::TqModbusRegister modbusRegister, const QVector<quint16> &data) void IntegrationPluginWebasto::onReceivedRegister(Webasto::TqModbusRegister modbusRegister, const QVector<quint16> &data)
{ {
Webasto *connection = static_cast<Webasto *>(sender()); Webasto *connection = static_cast<Webasto *>(sender());
Thing *thing = m_webastoConnections.key(connection); Thing *thing = m_webastoLiveConnections.key(connection);
if (!thing) { if (!thing) {
qCWarning(dcWebasto()) << "On basic information received, thing not found for connection"; qCWarning(dcWebasto()) << "On basic information received, thing not found for connection";
return; return;

View File

@ -1,6 +1,6 @@
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
* *
* Copyright 2013 - 2020, nymea GmbH * Copyright 2013 - 2023, nymea GmbH
* Contact: contact@nymea.io * Contact: contact@nymea.io
* *
* This file is part of nymea. * This file is part of nymea.
@ -31,14 +31,16 @@
#ifndef INTEGRATIONPLUGINWEBASTO_H #ifndef INTEGRATIONPLUGINWEBASTO_H
#define INTEGRATIONPLUGINWEBASTO_H #define INTEGRATIONPLUGINWEBASTO_H
#include <integrations/integrationplugin.h>
#include <plugintimer.h> #include <plugintimer.h>
#include <integrations/integrationplugin.h>
#include <network/networkdevicemonitor.h>
#include "webasto.h" #include "webasto.h"
#include "webastonextmodbustcpconnection.h"
#include <QUuid>
#include <QObject> #include <QObject>
#include <QHostAddress> #include <QHostAddress>
#include <QUuid>
class IntegrationPluginWebasto : public IntegrationPlugin class IntegrationPluginWebasto : public IntegrationPlugin
{ {
@ -53,17 +55,25 @@ public:
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 postSetupThing(Thing *thing) override;
void executeAction(ThingActionInfo *info) override;
void thingRemoved(Thing *thing) override; void thingRemoved(Thing *thing) override;
void executeAction(ThingActionInfo *info) override;
private: private:
PluginTimer *m_pluginTimer = nullptr; PluginTimer *m_pluginTimer = nullptr;
QHash<Thing *, Webasto *> m_webastoConnections;
QHash<QUuid, ThingActionInfo *> m_asyncActions; QHash<QUuid, ThingActionInfo *> m_asyncActions;
QHash<Thing *, Webasto *> m_webastoLiveConnections;
QHash<Thing *, WebastoNextModbusTcpConnection *> m_webastoNextConnections;
QHash<Thing *, NetworkDeviceMonitor *> m_monitors;
void setupWebastoNextConnection(ThingSetupInfo *info);
void update(Webasto *webasto); void update(Webasto *webasto);
void evaluatePhaseCount(Thing *thing); void evaluatePhaseCount(Thing *thing);
void executeWebastoNextPowerAction(ThingActionInfo *info, bool power);
private slots: private slots:
void onConnectionChanged(bool connected); void onConnectionChanged(bool connected);
void onWriteRequestExecuted(const QUuid &requestId, bool success); void onWriteRequestExecuted(const QUuid &requestId, bool success);

View File

@ -10,7 +10,7 @@
"thingClasses": [ "thingClasses": [
{ {
"id": "48472124-3199-4827-990a-b72069bd5658", "id": "48472124-3199-4827-990a-b72069bd5658",
"displayName": "Live Wallbox", "displayName": "Webasto Live",
"name": "webastoLive", "name": "webastoLive",
"createMethods": ["discovery"], "createMethods": ["discovery"],
"interfaces": ["evcharger", "smartmeterconsumer", "connectable"], "interfaces": ["evcharger", "smartmeterconsumer", "connectable"],
@ -35,7 +35,6 @@
{ {
"id": "7e6ed2b4-aa8a-4bf6-b20b-84ecc6cc1508", "id": "7e6ed2b4-aa8a-4bf6-b20b-84ecc6cc1508",
"displayName": "Connected", "displayName": "Connected",
"displayNameEvent": "Connected changed",
"name": "connected", "name": "connected",
"type": "bool", "type": "bool",
"defaultValue": false, "defaultValue": false,
@ -45,7 +44,6 @@
"id": "b076353b-e911-444f-80ad-3f78c4075d1a", "id": "b076353b-e911-444f-80ad-3f78c4075d1a",
"name": "chargePointState", "name": "chargePointState",
"displayName": "Charge point state", "displayName": "Charge point state",
"displayNameEvent": "Charge point state changed",
"type": "QString", "type": "QString",
"possibleValues": [ "possibleValues": [
"No vehicle attached", "No vehicle attached",
@ -65,7 +63,6 @@
"id": "a1a452f9-de93-4c31-b71b-c74264f85a3e", "id": "a1a452f9-de93-4c31-b71b-c74264f85a3e",
"name": "cableState", "name": "cableState",
"displayName": "Cable state", "displayName": "Cable state",
"displayNameEvent": "Cable state changed",
"type": "QString", "type": "QString",
"possibleValues": [ "possibleValues": [
"No cable attached", "No cable attached",
@ -82,7 +79,6 @@
"type": "bool", "type": "bool",
"defaultValue": false, "defaultValue": false,
"displayNameAction": "Start charging", "displayNameAction": "Start charging",
"displayNameEvent": "Charging status changed",
"writable": true "writable": true
}, },
{ {
@ -90,7 +86,6 @@
"name": "maxChargingCurrent", "name": "maxChargingCurrent",
"displayName": "Charging current", "displayName": "Charging current",
"displayNameAction": "Set charging current", "displayNameAction": "Set charging current",
"displayNameEvent": "Charging current changed",
"type": "uint", "type": "uint",
"unit": "Ampere", "unit": "Ampere",
"minValue": 6, "minValue": 6,
@ -102,7 +97,6 @@
"id": "0e15e78e-a233-4026-a0fd-f65edc824f1e", "id": "0e15e78e-a233-4026-a0fd-f65edc824f1e",
"name": "pluggedIn", "name": "pluggedIn",
"displayName": "Car plugged in", "displayName": "Car plugged in",
"displayNameEvent": "Car plugged in changed",
"type": "bool", "type": "bool",
"defaultValue": false "defaultValue": false
}, },
@ -110,7 +104,6 @@
"id": "8f35404d-8237-4ff8-8774-9ad10ceee5c3", "id": "8f35404d-8237-4ff8-8774-9ad10ceee5c3",
"name": "charging", "name": "charging",
"displayName": "Charging", "displayName": "Charging",
"displayNameEvent": "Charging changed",
"type": "bool", "type": "bool",
"defaultValue": false "defaultValue": false
}, },
@ -118,7 +111,6 @@
"id": "179e6136-2ac1-4247-b457-f804e2212293", "id": "179e6136-2ac1-4247-b457-f804e2212293",
"name": "phaseCount", "name": "phaseCount",
"displayName": "Number of connected phases", "displayName": "Number of connected phases",
"displayNameEvent": "Number of connected phases changed",
"type": "uint", "type": "uint",
"minValue": 1, "minValue": 1,
"maxValue": 3, "maxValue": 3,
@ -128,7 +120,6 @@
"id": "2027fbb6-c9d2-4a75-bdd0-a3ad3785cdc6", "id": "2027fbb6-c9d2-4a75-bdd0-a3ad3785cdc6",
"name": "currentPhase1", "name": "currentPhase1",
"displayName": "Current phase 1", "displayName": "Current phase 1",
"displayNameEvent": "Current phase 1 changed",
"type": "double", "type": "double",
"unit": "Ampere", "unit": "Ampere",
"defaultValue": 0.00 "defaultValue": 0.00
@ -137,7 +128,6 @@
"id": "1793f645-d7db-4e99-af92-3587aa3069f3", "id": "1793f645-d7db-4e99-af92-3587aa3069f3",
"name": "currentPhase2", "name": "currentPhase2",
"displayName": "Current phase 2", "displayName": "Current phase 2",
"displayNameEvent": "Current phase 2 changed",
"type": "double", "type": "double",
"unit": "Ampere", "unit": "Ampere",
"defaultValue": 0.00 "defaultValue": 0.00
@ -146,7 +136,6 @@
"id": "feb8c5da-91a7-45f9-acc3-c1b61478c3d2", "id": "feb8c5da-91a7-45f9-acc3-c1b61478c3d2",
"name": "currentPhase3", "name": "currentPhase3",
"displayName": "Current phase 3", "displayName": "Current phase 3",
"displayNameEvent": "Current phase 3 changed",
"type": "double", "type": "double",
"unit": "Ampere", "unit": "Ampere",
"defaultValue": 0.00 "defaultValue": 0.00
@ -155,7 +144,6 @@
"id": "b20a46ee-0f22-4096-a348-34e68e99e0be", "id": "b20a46ee-0f22-4096-a348-34e68e99e0be",
"name": "currentPower", "name": "currentPower",
"displayName": "Current power consumption", "displayName": "Current power consumption",
"displayNameEvent": "Current power consumtion changed",
"type": "double", "type": "double",
"unit": "Watt", "unit": "Watt",
"defaultValue": 0.00 "defaultValue": 0.00
@ -164,7 +152,6 @@
"id": "80568c51-054c-4351-b9d2-e875fee4cc1f", "id": "80568c51-054c-4351-b9d2-e875fee4cc1f",
"name": "totalEnergyConsumed", "name": "totalEnergyConsumed",
"displayName": "Total energy consumed", "displayName": "Total energy consumed",
"displayNameEvent": "Total energy consumption changed",
"type": "double", "type": "double",
"unit": "KiloWattHour", "unit": "KiloWattHour",
"defaultValue": 0 "defaultValue": 0
@ -173,7 +160,6 @@
"id": "87c70567-794e-4af2-916c-b34cf864afcf", "id": "87c70567-794e-4af2-916c-b34cf864afcf",
"name": "sessionTime", "name": "sessionTime",
"displayName": "Session time", "displayName": "Session time",
"displayNameEvent": "Session time changed",
"type": "int", "type": "int",
"unit": "Minutes", "unit": "Minutes",
"defaultValue": 0 "defaultValue": 0
@ -182,7 +168,6 @@
"id": "b9b46920-55c1-4bfa-9200-acdc9c0a2471", "id": "b9b46920-55c1-4bfa-9200-acdc9c0a2471",
"name": "sessionEnergy", "name": "sessionEnergy",
"displayName": "Session energy", "displayName": "Session energy",
"displayNameEvent": "Session energy changed",
"type": "double", "type": "double",
"unit": "KiloWattHour", "unit": "KiloWattHour",
"defaultValue": 0 "defaultValue": 0
@ -191,7 +176,6 @@
"id": "56d31fd1-5cfb-42dd-8181-e6b0d0ca9c8a", "id": "56d31fd1-5cfb-42dd-8181-e6b0d0ca9c8a",
"name": "error", "name": "error",
"displayName": "Error ", "displayName": "Error ",
"displayNameEvent": "Error changed",
"type": "int", "type": "int",
"defaultValue": 0 "defaultValue": 0
}, },
@ -199,7 +183,6 @@
"id": "0e60b15d-2b0c-4672-960e-7c6ea67bf7ea", "id": "0e60b15d-2b0c-4672-960e-7c6ea67bf7ea",
"name": "maxPossibleChargingCurrent", "name": "maxPossibleChargingCurrent",
"displayName": "Maximum possible charging current", "displayName": "Maximum possible charging current",
"displayNameEvent": "Maximum possible charging current changed",
"type": "double", "type": "double",
"unit": "Ampere", "unit": "Ampere",
"defaultValue": 6.00 "defaultValue": 6.00
@ -208,7 +191,242 @@
"id": "48b62082-f286-433e-9cf8-2dcf6c0ea248", "id": "48b62082-f286-433e-9cf8-2dcf6c0ea248",
"name": "userId", "name": "userId",
"displayName": "User ID", "displayName": "User ID",
"displayNameEvent": "User ID changed", "type": "QString",
"defaultValue": ""
}
]
},
{
"id": "1dddfbf4-a49d-4e28-8cbc-108547a369a2",
"displayName": "Webasto NEXT",
"name": "webastoNext",
"createMethods": ["discovery"],
"interfaces": ["evcharger", "smartmeterconsumer", "connectable"],
"settingsTypes": [
{
"id": "5292e079-515c-47ae-9117-6a70d5c02566",
"name": "safeCurrent",
"displayName": "Maximum current on communication failure",
"type": "uint",
"defaultValue": "6",
"minValue": 6,
"maxValue": 16,
"unit": "Ampere"
},
{
"id": "20710f47-d585-40fa-a9bd-8b586711966e",
"name": "communicationTimeout",
"displayName": "Communication timeout",
"type": "uint",
"defaultValue": 60,
"minValue": 1,
"unit": "Seconds"
}
],
"paramTypes": [
{
"id": "882b662f-ec7c-4134-be31-5d36567b9fc2",
"name": "macAddress",
"displayName": "MAC address",
"type": "QString",
"defaultValue": "",
"readOnly": true
},
{
"id": "be5a0c50-f3ba-4562-b6c0-a0208e2ab118",
"name":"port",
"displayName": "Port",
"type": "uint",
"defaultValue": 502
},
{
"id": "bdb8a7bb-fcfd-4130-b860-ba3eaa3f9932",
"name":"slaveId",
"displayName": "Slave ID",
"type": "uint",
"defaultValue": 1
}
],
"stateTypes": [
{
"id": "291a55e5-2f63-42bc-b0aa-cf2079a19632",
"displayName": "Connected",
"name": "connected",
"type": "bool",
"defaultValue": false,
"cached": false
},
{
"id": "de752511-b47d-4abc-980a-51c261a93a69",
"name": "power",
"displayName": "Charging enabled",
"displayNameAction": "Enable charging",
"type": "bool",
"defaultValue": false,
"writable": true
},
{
"id": "62bfa06d-599c-4a3b-8f51-89e307a25ca6",
"name": "maxChargingCurrent",
"displayName": "Charging current",
"displayNameAction": "Set charging current",
"type": "uint",
"unit": "Ampere",
"minValue": 6,
"maxValue": 32,
"defaultValue": 6,
"writable": true
},
{
"id": "e30dc786-6c01-4a86-9f72-8d32df00f528",
"name": "pluggedIn",
"displayName": "Car plugged in",
"type": "bool",
"defaultValue": false,
"suggestLogging": true
},
{
"id": "c886d4a6-20fb-4aad-ad95-8b16aa6c8363",
"name": "charging",
"displayName": "Charging",
"type": "bool",
"defaultValue": false,
"suggestLogging": true
},
{
"id": "ba17d0d4-bfed-4920-b85e-54b34200bfff",
"name": "phaseCount",
"displayName": "Number of connected phases",
"type": "uint",
"minValue": 1,
"maxValue": 3,
"defaultValue": 1
},
{
"id": "d211886e-e755-4e7c-b95d-69e88e5be229",
"name": "usedPhases",
"displayName": "Phases used for charging",
"type": "QString",
"possibleValues" : ["", "A", "B", "C", "AB", "AC", "BC", "ABC" ],
"defaultValue": ""
},
{
"id": "584e1ae7-2844-44a9-a6f7-183ee0d595f1",
"name": "currentPower",
"displayName": "Charging power",
"type": "double",
"unit": "Watt",
"defaultValue": 0.00
},
{
"id": "9a858704-9525-4480-88ff-59ba0014daa1",
"name": "totalEnergyConsumed",
"displayName": "Total energy consumed",
"type": "double",
"unit": "KiloWattHour",
"defaultValue": 0
},
{
"id": "8fab231b-0270-4528-81b0-84c89b8ced1c",
"name": "currentPhaseA",
"displayName": "Current phase A",
"type": "double",
"unit": "Ampere",
"defaultValue": 0.00
},
{
"id": "c4f4b78b-e220-4c49-9019-4d1dc0563f89",
"name": "currentPhaseB",
"displayName": "Current phase B",
"type": "double",
"unit": "Ampere",
"defaultValue": 0.00
},
{
"id": "a8ef8fae-5ff4-4381-9341-cc8910d415f4",
"name": "currentPhaseC",
"displayName": "Current phase C",
"type": "double",
"unit": "Ampere",
"defaultValue": 0.00
},
{
"id": "cb903571-9b0b-4a86-9840-112ec76088c5",
"name": "currentPowerPhaseA",
"displayName": "Current power phase A",
"type": "double",
"unit": "Watt",
"defaultValue": 0.00
},
{
"id": "d32c6b2c-0eae-4bbc-8d04-2a00a30de864",
"name": "currentPowerPhaseB",
"displayName": "Current power phase B",
"type": "double",
"unit": "Watt",
"defaultValue": 0.00
},
{
"id": "fc98ffe8-4824-4db5-96bb-62dfef6e0b34",
"name": "currentPowerPhaseC",
"displayName": "Current power phase C",
"type": "double",
"unit": "Watt",
"defaultValue": 0.00
},
{
"id": "52a7a45c-bdec-49ed-9a1b-4eebff5b1482",
"name": "maxCurrentTotal",
"displayName": "Maximum current overall",
"type": "uint",
"unit": "Ampere",
"defaultValue": 0
},
{
"id": "f7d0f75f-5313-4d73-9420-eb776f9da3d5",
"name": "minCurrentTotal",
"displayName": "Minimum current overall",
"type": "uint",
"unit": "Ampere",
"defaultValue": 0
},
{
"id": "e9b2a3c1-3a4b-4cb7-b253-ae9b4b8862f9",
"name": "maxCurrentCharger",
"displayName": "Maximum current charger",
"type": "uint",
"unit": "Ampere",
"defaultValue": 0
},
{
"id": "d8c2c93f-2219-4b69-b7c0-c983c9d69232",
"name": "maxCurrentCable",
"displayName": "Maximum current cable",
"type": "uint",
"unit": "Ampere",
"defaultValue": 0
},
{
"id": "3a2239fd-09c1-46ac-9dcb-5e08733f862c",
"name": "maxCurrentElectricVehicle",
"displayName": "Maximum current electric vehicle",
"type": "uint",
"unit": "Ampere",
"defaultValue": 0
},
{
"id": "78e8262a-5d41-4749-ab8a-a50d5c661cbb",
"name": "sessionEnergy",
"displayName": "Session energy",
"type": "double",
"unit": "KiloWattHour",
"defaultValue": 0.0,
"suggestLogging": true
},
{
"id": "54feab4b-3134-4968-bfec-d2d656cc4ad6",
"name": "error",
"displayName": "Error",
"type": "QString", "type": "QString",
"defaultValue": "" "defaultValue": ""
} }
@ -218,4 +436,3 @@
} }
] ]
} }

View File

@ -0,0 +1,411 @@
{
"className": "WebastoNext",
"protocol": "TCP",
"endianness": "BigEndian",
"errorLimitUntilNotReachable": 10,
"checkReachableRegister": "totalActivePower",
"enums": [
{
"name": "ChargerState",
"values": [
{
"key": "NoVehicle",
"value": 0
},
{
"key": "VehicleAttachedNoPermission",
"value": 1
},
{
"key": "Charging",
"value": 3
},
{
"key": "ChargingPaused",
"value": 4
},
{
"key": "ChargingError",
"value": 7
},
{
"key": "ChargingStationReserved",
"value": 8
}
]
},
{
"name": "ChargeState",
"values": [
{
"key": "Idle",
"value": 0
},
{
"key": "Charging",
"value": 1
}
]
},
{
"name": "EvseState",
"values": [
{
"key": "Starting",
"value": 0
},
{
"key": "Running",
"value": 1
},
{
"key": "Error",
"value": 2
}
]
},
{
"name": "CableState",
"values": [
{
"key": "NoCableAttached",
"value": 0
},
{
"key": "CableAttachedNoCar",
"value": 1
},
{
"key": "CableAttachedCarAttached",
"value": 2
},
{
"key": "CableAttachedCarAttachedLocked",
"value": 3
}
]
},
{
"name": "ChargingAction",
"values": [
{
"key": "NoAction",
"value": 0
},
{
"key": "StartSession",
"value": 1
},
{
"key": "CancelSession",
"value": 2
}
]
}
],
"blocks": [
{
"id": "states",
"readSchedule": "update",
"registers": [
{
"id": "chargerState",
"description": "State of the charging device",
"address": 1000,
"size": 1,
"type": "uint16",
"registerType": "holdingRegister",
"enum": "ChargerState",
"readSchedule": "update",
"defaultValue": "ChargerStateNoVehicle",
"access": "RO"
},
{
"id": "chargeState",
"description": "Charge state",
"address": 1001,
"size": 1,
"type": "uint16",
"registerType": "holdingRegister",
"enum": "ChargeState",
"readSchedule": "update",
"defaultValue": "ChargeStateIdle",
"access": "RO"
},
{
"id": "evseState",
"description": "EVSE state (state of charging station)",
"address": 1002,
"size": 1,
"type": "uint16",
"registerType": "holdingRegister",
"enum": "EvseState",
"readSchedule": "update",
"defaultValue": "EvseStateStarting",
"access": "RO"
}
]
}
],
"registers": [
{
"id": "cableState",
"description": "Cable state",
"address": 1004,
"size": 1,
"type": "uint16",
"registerType": "holdingRegister",
"enum": "CableState",
"readSchedule": "update",
"defaultValue": "CableStateNoCableAttached",
"access": "RO"
},
{
"id": "evseErrorCode",
"description": "ESVE Error codes, 0 = No error",
"address": 1006,
"size": 1,
"type": "uint16",
"registerType": "holdingRegister",
"readSchedule": "update",
"defaultValue": "CableStateNoCableAttached",
"access": "RO"
},
{
"id": "currentL1",
"description": "Charging current L1",
"address": 1008,
"size": 1,
"type": "uint16",
"unit": "mA",
"registerType": "holdingRegister",
"readSchedule": "update",
"defaultValue": "0",
"access": "RO"
},
{
"id": "currentL2",
"description": "Charging current L2",
"address": 1010,
"size": 1,
"type": "uint16",
"unit": "mA",
"registerType": "holdingRegister",
"readSchedule": "update",
"defaultValue": "0",
"access": "RO"
},
{
"id": "currentL3",
"description": "Charging current L3",
"address": 1012,
"size": 1,
"type": "uint16",
"unit": "mA",
"registerType": "holdingRegister",
"readSchedule": "update",
"defaultValue": "0",
"access": "RO"
},
{
"id": "totalActivePower",
"description": "Total active charging power",
"address": 1020,
"size": 2,
"type": "uint32",
"unit": "W",
"registerType": "holdingRegister",
"readSchedule": "update",
"defaultValue": "0",
"access": "RO"
},
{
"id": "activePowerL1",
"description": "Active power L1",
"address": 1024,
"size": 2,
"type": "uint32",
"unit": "W",
"registerType": "holdingRegister",
"readSchedule": "update",
"defaultValue": "0",
"access": "RO"
},
{
"id": "activePowerL2",
"description": "Active power L2",
"address": 1028,
"size": 2,
"type": "uint32",
"unit": "W",
"registerType": "holdingRegister",
"readSchedule": "update",
"defaultValue": "0",
"access": "RO"
},
{
"id": "activePowerL3",
"description": "Active power L3",
"address": 1032,
"size": 2,
"type": "uint32",
"unit": "W",
"registerType": "holdingRegister",
"readSchedule": "update",
"defaultValue": "0",
"access": "RO"
},
{
"id": "energyConsumed",
"description": "Energy meter reading of the charging station",
"address": 1036,
"size": 2,
"type": "uint32",
"unit": "Wh",
"registerType": "holdingRegister",
"readSchedule": "update",
"defaultValue": "0",
"access": "RO"
},
{
"id": "maxChargingCurrent",
"description": "The maximal charging current of the hardware (EVSE, Cable, EV)",
"address": 1100,
"size": 1,
"type": "uint16",
"unit": "A",
"registerType": "holdingRegister",
"readSchedule": "update",
"defaultValue": "0",
"access": "RO"
},
{
"id": "minChargingCurrent",
"description": "The minimal charging current of the hardware (EVSE, Cable, EV)",
"address": 1102,
"size": 1,
"type": "uint16",
"unit": "A",
"registerType": "holdingRegister",
"readSchedule": "update",
"defaultValue": "0",
"access": "RO"
},
{
"id": "maxChargingCurrentStation",
"description": "The maximal charging current of the station",
"address": 1104,
"size": 1,
"type": "uint16",
"unit": "A",
"registerType": "holdingRegister",
"readSchedule": "update",
"defaultValue": "0",
"access": "RO"
},
{
"id": "maxChargingCurrentCable",
"description": "The maximal charging current of the cable",
"address": 1106,
"size": 1,
"type": "uint16",
"unit": "A",
"registerType": "holdingRegister",
"readSchedule": "update",
"defaultValue": "0",
"access": "RO"
},
{
"id": "maxChargingCurrentEv",
"description": "The maximal charging current of the EV",
"address": 1108,
"size": 1,
"type": "uint16",
"unit": "A",
"registerType": "holdingRegister",
"readSchedule": "update",
"defaultValue": "0",
"access": "RO"
},
{
"id": "sessionEnergy",
"description": "Sum of charged energy for the last session",
"address": 1502,
"size": 1,
"type": "uint16",
"unit": "Wh",
"registerType": "holdingRegister",
"readSchedule": "update",
"defaultValue": "0",
"access": "RO"
},
{
"id": "safeCurrent",
"description": "Max. charge current under communication failure",
"address": 2000,
"size": 1,
"type": "uint16",
"unit": "A",
"readSchedule": "update",
"registerType": "holdingRegister",
"defaultValue": "0",
"access": "RW"
},
{
"id": "comTimeout",
"description": "Communication timeout",
"address": 2002,
"size": 1,
"type": "uint16",
"unit": "s",
"readSchedule": "update",
"registerType": "holdingRegister",
"defaultValue": "0",
"access": "RW"
},
{
"id": "chargePower",
"description": "Set the charge power",
"address": 5000,
"size": 2,
"type": "uint32",
"unit": "W",
"registerType": "holdingRegister",
"defaultValue": "0",
"access": "WO"
},
{
"id": "chargeCurrent",
"description": "Set the charge current",
"address": 5004,
"size": 1,
"type": "uint16",
"unit": "A",
"registerType": "holdingRegister",
"defaultValue": "0",
"access": "WO"
},
{
"id": "chargingAction",
"description": "Start / Cancel charging session",
"address": 5006,
"size": 1,
"type": "uint16",
"enum": "ChargingAction",
"registerType": "holdingRegister",
"defaultValue": "0",
"access": "WO"
},
{
"id": "lifeBit",
"description": "Life bit",
"address": 6000,
"size": 1,
"type": "uint16",
"readSchedule": "update",
"registerType": "holdingRegister",
"defaultValue": "0",
"access": "RW"
}
]
}

View File

@ -1,10 +1,16 @@
include(../plugins.pri) include(../plugins.pri)
# Generate modbus connection
MODBUS_CONNECTIONS += webasto-next-registers.json
MODBUS_TOOLS_CONFIG += VERBOSE
include(../modbus.pri) include(../modbus.pri)
SOURCES += \ SOURCES += \
integrationpluginwebasto.cpp \ integrationpluginwebasto.cpp \
webasto.cpp webasto.cpp \
webastodiscovery.cpp
HEADERS += \ HEADERS += \
integrationpluginwebasto.h \ integrationpluginwebasto.h \
webasto.h webasto.h \
webastodiscovery.h

View File

@ -0,0 +1,197 @@
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
*
* 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 "webastodiscovery.h"
#include "extern-plugininfo.h"
WebastoDiscovery::WebastoDiscovery(NetworkDeviceDiscovery *networkDeviceDiscovery, QObject *parent)
: QObject{parent},
m_networkDeviceDiscovery{networkDeviceDiscovery}
{
}
void WebastoDiscovery::startDiscovery()
{
// TODO: add parameter for searching WebastoNext or WebastoLive, for now the discovery searches only for WebastoNext
m_startDateTime = QDateTime::currentDateTime();
qCInfo(dcWebasto()) << "Discovery: Starting to search for WebastoNext wallboxes in the network...";
NetworkDeviceDiscoveryReply *discoveryReply = m_networkDeviceDiscovery->discover();
connect(discoveryReply, &NetworkDeviceDiscoveryReply::networkDeviceInfoAdded, this, &WebastoDiscovery::checkNetworkDevice);
connect(discoveryReply, &NetworkDeviceDiscoveryReply::finished, discoveryReply, &NetworkDeviceDiscoveryReply::deleteLater);
connect(discoveryReply, &NetworkDeviceDiscoveryReply::finished, this, [=](){
qCDebug(dcWebasto()) << "Discovery: Network discovery finished. Found" << discoveryReply->networkDeviceInfos().count() << "network devices";
// Give the last connections added right before the network discovery finished a chance to check the device...
QTimer::singleShot(3000, this, [this](){
qCDebug(dcWebasto()) << "Discovery: Grace period timer triggered.";
finishDiscovery();
});
});
}
QList<WebastoDiscovery::Result> WebastoDiscovery::results() const
{
return m_results;
}
void WebastoDiscovery::checkNetworkDevice(const NetworkDeviceInfo &networkDeviceInfo)
{
WebastoNextModbusTcpConnection *connection = new WebastoNextModbusTcpConnection(networkDeviceInfo.address(), 502, 1, this);
m_connections.append(connection);
connect(connection, &WebastoNextModbusTcpConnection::reachableChanged, this, [=](bool reachable){
if (!reachable) {
// Disconnected ... done with this connection
cleanupConnection(connection);
return;
}
// Read some well known registers to verify if the register exist and make sense...
QModbusReply *reply = connection->readCableState();
connect(reply, &QModbusReply::finished, this, [=](){
reply->deleteLater();
if (reply->error() != QModbusDevice::NoError) {
// Something went wrong...probably not the device we are searching for
cleanupConnection(connection);
return;
}
// Make sure this is a valid cable state
const QModbusDataUnit unit = reply->result();
quint16 rawValue = ModbusDataUtils::convertToUInt16(unit.values());
QMetaEnum valueEnum = WebastoNextModbusTcpConnection::staticMetaObject.enumerator(WebastoNextModbusTcpConnection::staticMetaObject.indexOfEnumerator("CableState"));
if (!valueEnum.valueToKey(rawValue)) {
qCDebug(dcWebasto()) << "Discovery: invalid enum value for cable state on connection on" << networkDeviceInfo.address().toString() << "Continue...";;
cleanupConnection(connection);
}
QModbusReply *reply = connection->readChargerState();
connect(reply, &QModbusReply::finished, this, [=](){
reply->deleteLater();
if (reply->error() != QModbusDevice::NoError) {
// Something went wrong...probably not the device we are searching for
cleanupConnection(connection);
return;
}
// Make sure this is a valid charger state
const QModbusDataUnit unit = reply->result();
quint16 rawValue = ModbusDataUtils::convertToUInt16(unit.values());
QMetaEnum valueEnum = WebastoNextModbusTcpConnection::staticMetaObject.enumerator(WebastoNextModbusTcpConnection::staticMetaObject.indexOfEnumerator("ChargerState"));
if (!valueEnum.valueToKey(rawValue)) {
qCDebug(dcWebasto()) << "Discovery: invalid enum value for charger state on connection on" << networkDeviceInfo.address().toString() << "Continue...";;
cleanupConnection(connection);
}
// Read some registers distributed over the range...
QModbusReply *reply = connection->readTotalActivePower();
connect(reply, &QModbusReply::finished, this, [=](){
reply->deleteLater();
if (reply->error() != QModbusDevice::NoError) {
// Something went wrong...probably not the device we are searching for
cleanupConnection(connection);
return;
}
QModbusReply *reply = connection->readSessionEnergy();
connect(reply, &QModbusReply::finished, this, [=](){
reply->deleteLater();
if (reply->error() != QModbusDevice::NoError) {
// Something went wrong...probably not the device we are searching for
cleanupConnection(connection);
return;
}
// All values good so far, let's assume this is a Webasto NEXT
Result result;
result.productName = "Webasto NEXT";
result.type = TypeWebastoNext;
result.networkDeviceInfo = networkDeviceInfo;
m_results.append(result);
qCDebug(dcWebasto()) << "Discovery: --> Found" << result.productName << result.networkDeviceInfo;
// Done with this connection
cleanupConnection(connection);
});
});
});
});
});
// If we get any error...skip this host...
connect(connection->modbusTcpMaster(), &ModbusTcpMaster::connectionErrorOccurred, this, [=](QModbusDevice::Error error){
if (error != QModbusDevice::NoError) {
qCDebug(dcWebasto()) << "Discovery: Connection error on" << networkDeviceInfo.address().toString() << "Continue...";;
cleanupConnection(connection);
}
});
// If check reachability failed...skip this host...
connect(connection, &WebastoNextModbusTcpConnection::checkReachabilityFailed, this, [=](){
qCDebug(dcWebasto()) << "Discovery: Check reachability failed on" << networkDeviceInfo.address().toString() << "Continue...";;
cleanupConnection(connection);
});
// Try to connect, maybe it works, maybe not...
connection->connectDevice();
}
void WebastoDiscovery::cleanupConnection(WebastoNextModbusTcpConnection *connection)
{
m_connections.removeAll(connection);
connection->disconnectDevice();
connection->deleteLater();
}
void WebastoDiscovery::finishDiscovery()
{
qint64 durationMilliSeconds = QDateTime::currentMSecsSinceEpoch() - m_startDateTime.toMSecsSinceEpoch();
// Cleanup any leftovers...we don't care any more
foreach (WebastoNextModbusTcpConnection *connection, m_connections)
cleanupConnection(connection);
qCInfo(dcWebasto()) << "Discovery: Finished the discovery process. Found" << m_results.count() << "Webasto NEXT wallboxes in" << QTime::fromMSecsSinceStartOfDay(durationMilliSeconds).toString("mm:ss.zzz");
emit discoveryFinished();
}

View File

@ -0,0 +1,80 @@
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
*
* 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 WEBASTODISCOVERY_H
#define WEBASTODISCOVERY_H
#include <QObject>
#include <network/networkdevicediscovery.h>
#include "webastonextmodbustcpconnection.h"
class WebastoDiscovery : public QObject
{
Q_OBJECT
public:
enum Type {
TypeWebastoLive,
TypeWebastoNext
};
Q_ENUM(Type)
typedef struct Result {
QString productName;
Type type;
NetworkDeviceInfo networkDeviceInfo;
} Result;
explicit WebastoDiscovery(NetworkDeviceDiscovery *networkDeviceDiscovery, QObject *parent = nullptr);
void startDiscovery();
QList<Result> results() const;
signals:
void discoveryFinished();
private:
NetworkDeviceDiscovery *m_networkDeviceDiscovery = nullptr;
QList<WebastoNextModbusTcpConnection *> m_connections;
QList<Result> m_results;
QDateTime m_startDateTime;
void checkNetworkDevice(const NetworkDeviceInfo &networkDeviceInfo);
void cleanupConnection(WebastoNextModbusTcpConnection *connection);
void finishDiscovery();
};
#endif // WEBASTODISCOVERY_H