Webasto: Add webasto NEXT support

pull/120/head
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();
}
if (m_modbusTcpClient) {
disconnectDevice();
}
disconnectDevice();
}
QHostAddress ModbusTcpMaster::hostAddress() const
@ -78,19 +76,25 @@ void ModbusTcpMaster::setPort(uint 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)
{
m_hostAddress = hostAddress;
}
bool ModbusTcpMaster::connectDevice() {
bool ModbusTcpMaster::connectDevice()
{
// TCP connection to target device
if (!m_modbusTcpClient)
return false;
// Only connect if we are in the unconnected state
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::NetworkAddressParameter, m_hostAddress.toString());
m_modbusTcpClient->setTimeout(m_timeout);
@ -98,9 +102,10 @@ bool ModbusTcpMaster::connectDevice() {
return m_modbusTcpClient->connectDevice();
} else if (m_modbusTcpClient->state() != QModbusDevice::ConnectedState) {
// Restart the timer in case of connecting not finished yet or closing
qCDebug(dcModbusTcpMaster()) << "Starting the reconnect mechanism timer";
m_reconnectTimer->start();
} 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;
@ -108,9 +113,6 @@ bool ModbusTcpMaster::connectDevice() {
void ModbusTcpMaster::disconnectDevice()
{
if (!m_modbusTcpClient)
return;
// Stop the reconnect timer since disconnect was explicitly called
m_reconnectTimer->stop();
m_modbusTcpClient->disconnectDevice();
@ -118,10 +120,7 @@ void ModbusTcpMaster::disconnectDevice()
bool ModbusTcpMaster::reconnectDevice()
{
qCWarning(dcModbusTcpMaster()) << "Reconnecting modbus TCP device" << QString("%1:%2").arg(m_hostAddress.toString()).arg(m_port);
if (!m_modbusTcpClient)
return false;
qCWarning(dcModbusTcpMaster()) << "Reconnecting modbus TCP device" << connectionUrl();
disconnectDevice();
return connectDevice();
}
@ -165,10 +164,6 @@ QModbusDevice::Error ModbusTcpMaster::error() const
QUuid ModbusTcpMaster::readCoil(uint slaveAddress, uint registerAddress, uint size)
{
if (!m_modbusTcpClient) {
return QUuid();
}
QUuid requestId = QUuid::createUuid();
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());
} else {
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){
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());
});
@ -198,7 +193,7 @@ QUuid ModbusTcpMaster::readCoil(uint slaveAddress, uint registerAddress, uint si
return QUuid();
}
} 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 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)
{
if (!m_modbusTcpClient) {
return QUuid();
}
QUuid requestId = QUuid::createUuid();
QModbusDataUnit request = QModbusDataUnit(QModbusDataUnit::RegisterType::HoldingRegisters, registerAddress, values.length());
request.setValues(values);
@ -224,13 +215,13 @@ QUuid ModbusTcpMaster::writeHoldingRegisters(uint slaveAddress, uint registerAdd
emit receivedHoldingRegister(reply->serverAddress(), modbusAddress, unit.values());
} else {
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();
});
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());
});
@ -240,7 +231,7 @@ QUuid ModbusTcpMaster::writeHoldingRegisters(uint slaveAddress, uint registerAdd
return QUuid();
}
} 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 requestId;
@ -268,9 +259,6 @@ QModbusReply *ModbusTcpMaster::sendWriteRequest(const QModbusDataUnit &write, in
QUuid ModbusTcpMaster::readDiscreteInput(uint slaveAddress, uint registerAddress, uint size)
{
if (!m_modbusTcpClient) {
return QUuid();
}
QUuid requestId = QUuid::createUuid();
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());
} else {
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){
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());
});
@ -301,7 +289,7 @@ QUuid ModbusTcpMaster::readDiscreteInput(uint slaveAddress, uint registerAddress
return QUuid();
}
} 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 requestId;
@ -328,12 +316,12 @@ QUuid ModbusTcpMaster::readInputRegister(uint slaveAddress, uint registerAddress
emit receivedInputRegister(reply->serverAddress(), modbusAddress, unit.values());
} else {
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){
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());
});
@ -344,7 +332,7 @@ QUuid ModbusTcpMaster::readInputRegister(uint slaveAddress, uint registerAddress
return QUuid();
}
} 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 requestId;
@ -352,10 +340,6 @@ QUuid ModbusTcpMaster::readInputRegister(uint slaveAddress, uint registerAddress
QUuid ModbusTcpMaster::readHoldingRegister(uint slaveAddress, uint registerAddress, uint size)
{
if (!m_modbusTcpClient) {
return QUuid();
}
QUuid requestId = QUuid::createUuid();
QModbusDataUnit request = QModbusDataUnit(QModbusDataUnit::RegisterType::HoldingRegisters, registerAddress, size);
@ -372,7 +356,7 @@ QUuid ModbusTcpMaster::readHoldingRegister(uint slaveAddress, uint registerAddre
} else {
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());
}
reply->deleteLater();
@ -380,7 +364,7 @@ QUuid ModbusTcpMaster::readHoldingRegister(uint slaveAddress, uint registerAddre
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());
});
@ -390,7 +374,7 @@ QUuid ModbusTcpMaster::readHoldingRegister(uint slaveAddress, uint registerAddre
return QUuid();
}
} 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 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)
{
if (!m_modbusTcpClient) {
return QUuid();
}
QUuid requestId = QUuid::createUuid();
QModbusDataUnit request = QModbusDataUnit(QModbusDataUnit::RegisterType::Coils, registerAddress, values.length());
request.setValues(values);
@ -424,13 +404,13 @@ QUuid ModbusTcpMaster::writeCoils(uint slaveAddress, uint registerAddress, const
} else {
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();
});
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());
});
@ -440,7 +420,7 @@ QUuid ModbusTcpMaster::writeCoils(uint slaveAddress, uint registerAddress, const
return QUuid();
}
} 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 requestId;
@ -453,7 +433,7 @@ QUuid ModbusTcpMaster::writeHoldingRegister(uint slaveAddress, uint registerAddr
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);
}

View File

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

View File

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

View File

@ -1,17 +1,21 @@
# Webasto
## Supported Things
Connects nymea to Webasto wallboxes. Currently supported models:
* AC Wallbox Live
* Webasto Live
* Webasto NEXT
## Requirements
* The packages 'nymea-plugin-webasto' must be installed.
* The modbus server must be enabled
* The setting 'Modbus Slave Register Address Set' must be set to 'TQ-DM100'
* The setting 'Modbus TCP Server Port Number' must be set to 502
nymea uses the Modbus TCP connection to connect to the wallbox.
The modbus server must be enabled on the Wallbox.
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
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
*
* This file is part of nymea.
@ -29,10 +29,12 @@
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
#include "integrationpluginwebasto.h"
#include "webastodiscovery.h"
#include "plugininfo.h"
#include <network/networkdevicediscovery.h>
#include <types/param.h>
#include <hardware/electricity.h>
#include <network/networkdevicediscovery.h>
#include <QDebug>
#include <QStringList>
@ -51,14 +53,14 @@ void IntegrationPluginWebasto::init()
void IntegrationPluginWebasto::discoverThings(ThingDiscoveryInfo *info)
{
if (info->thingClassId() == webastoLiveThingClassId) {
if (!hardwareManager()->networkDeviceDiscovery()->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."));
return;
}
if (!hardwareManager()->networkDeviceDiscovery()->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."));
return;
}
qCDebug(dcWebasto()) << "Discover things";
if (info->thingClassId() == webastoLiveThingClassId) {
qCInfo(dcWebasto()) << "Start discovering webasto live in the local network...";
NetworkDeviceDiscoveryReply *discoveryReply = hardwareManager()->networkDeviceDiscovery()->discover();
connect(discoveryReply, &NetworkDeviceDiscoveryReply::finished, discoveryReply, &NetworkDeviceDiscoveryReply::deleteLater);
connect(discoveryReply, &NetworkDeviceDiscoveryReply::finished, this, [=](){
@ -69,7 +71,7 @@ void IntegrationPluginWebasto::discoverThings(ThingDiscoveryInfo *info)
if (!networkDeviceInfo.hostName().contains("webasto", Qt::CaseSensitivity::CaseInsensitive))
continue;
QString title = "Wallbox ";
QString title = "Webasto Live";
if (networkDeviceInfo.hostName().isEmpty()) {
title += networkDeviceInfo.address().toString();
} else {
@ -100,9 +102,55 @@ void IntegrationPluginWebasto::discoverThings(ThingDiscoveryInfo *info)
}
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)
@ -112,14 +160,15 @@ void IntegrationPluginWebasto::setupThing(ThingSetupInfo *info)
if (thing->thingClassId() == webastoLiveThingClassId) {
if (m_webastoConnections.contains(thing)) {
if (m_webastoLiveConnections.contains(thing)) {
// Clean up after reconfiguration
m_webastoConnections.take(thing)->deleteLater();
m_webastoLiveConnections.take(thing)->deleteLater();
}
QHostAddress address = QHostAddress(thing->paramValue(webastoLiveThingIpAddressParamTypeId).toString());
Webasto *webasto = new Webasto(address, 502, thing);
m_webastoConnections.insert(thing, webasto);
connect(webasto, &Webasto::destroyed, this, [thing, this] {m_webastoConnections.remove(thing);});
m_webastoLiveConnections.insert(thing, webasto);
connect(webasto, &Webasto::destroyed, this, [thing, this] {m_webastoLiveConnections.remove(thing);});
connect(webasto, &Webasto::connectionStateChanged, this, &IntegrationPluginWebasto::onConnectionChanged);
connect(webasto, &Webasto::receivedRegister, this, &IntegrationPluginWebasto::onReceivedRegister);
connect(webasto, &Webasto::writeRequestError, this, &IntegrationPluginWebasto::onWriteRequestError);
@ -132,36 +181,142 @@ void IntegrationPluginWebasto::setupThing(ThingSetupInfo *info)
if (connected)
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)
{
qCDebug(dcWebasto()) << "Post setup thing" << thing->name();
if (!m_pluginTimer) {
qCDebug(dcWebasto()) << "Setting up refresh timer for Webasto connections";
m_pluginTimer = hardwareManager()->pluginTimerManager()->registerTimer(1);
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);
}
}
foreach(WebastoNextModbusTcpConnection *webastoNext, m_webastoNextConnections) {
if (webastoNext->reachable()) {
webastoNext->update();
}
}
});
m_pluginTimer->start();
}
if (thing->thingClassId() == webastoLiveThingClassId) {
Webasto *connection = m_webastoConnections.value(thing);
if (!connection) {
qCWarning(dcWebasto()) << "Can't find connection to thing";
}
Webasto *connection = m_webastoLiveConnections.value(thing);
update(connection);
return;
}
} else {
Q_ASSERT_X(false, "postSetupThing", QString("Unhandled thingClassId: %1").arg(thing->thingClassId().toString()).toUtf8());
if (thing->thingClassId() == webastoNextThingClassId) {
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)
{
Thing *thing = info->thing();
@ -169,7 +324,7 @@ void IntegrationPluginWebasto::executeAction(ThingActionInfo *info)
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";
return info->finish(Thing::ThingErrorHardwareNotAvailable);
@ -200,20 +355,338 @@ void IntegrationPluginWebasto::executeAction(ThingActionInfo *info)
} else {
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()) {
//Stop timer
}
void IntegrationPluginWebasto::setupWebastoNextConnection(ThingSetupInfo *info)
{
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)
@ -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)
{
Webasto *connection = static_cast<Webasto *>(sender());
Thing *thing = m_webastoConnections.key(connection);
Thing *thing = m_webastoLiveConnections.key(connection);
if (!thing) {
qCWarning(dcWebasto()) << "On connection changed, thing not found for connection";
return;
@ -290,7 +788,7 @@ void IntegrationPluginWebasto::onWriteRequestError(const QUuid &requestId, const
void IntegrationPluginWebasto::onReceivedRegister(Webasto::TqModbusRegister modbusRegister, const QVector<quint16> &data)
{
Webasto *connection = static_cast<Webasto *>(sender());
Thing *thing = m_webastoConnections.key(connection);
Thing *thing = m_webastoLiveConnections.key(connection);
if (!thing) {
qCWarning(dcWebasto()) << "On basic information received, thing not found for connection";
return;

View File

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

View File

@ -10,7 +10,7 @@
"thingClasses": [
{
"id": "48472124-3199-4827-990a-b72069bd5658",
"displayName": "Live Wallbox",
"displayName": "Webasto Live",
"name": "webastoLive",
"createMethods": ["discovery"],
"interfaces": ["evcharger", "smartmeterconsumer", "connectable"],
@ -35,7 +35,6 @@
{
"id": "7e6ed2b4-aa8a-4bf6-b20b-84ecc6cc1508",
"displayName": "Connected",
"displayNameEvent": "Connected changed",
"name": "connected",
"type": "bool",
"defaultValue": false,
@ -45,7 +44,6 @@
"id": "b076353b-e911-444f-80ad-3f78c4075d1a",
"name": "chargePointState",
"displayName": "Charge point state",
"displayNameEvent": "Charge point state changed",
"type": "QString",
"possibleValues": [
"No vehicle attached",
@ -65,7 +63,6 @@
"id": "a1a452f9-de93-4c31-b71b-c74264f85a3e",
"name": "cableState",
"displayName": "Cable state",
"displayNameEvent": "Cable state changed",
"type": "QString",
"possibleValues": [
"No cable attached",
@ -82,7 +79,6 @@
"type": "bool",
"defaultValue": false,
"displayNameAction": "Start charging",
"displayNameEvent": "Charging status changed",
"writable": true
},
{
@ -90,7 +86,6 @@
"name": "maxChargingCurrent",
"displayName": "Charging current",
"displayNameAction": "Set charging current",
"displayNameEvent": "Charging current changed",
"type": "uint",
"unit": "Ampere",
"minValue": 6,
@ -102,7 +97,6 @@
"id": "0e15e78e-a233-4026-a0fd-f65edc824f1e",
"name": "pluggedIn",
"displayName": "Car plugged in",
"displayNameEvent": "Car plugged in changed",
"type": "bool",
"defaultValue": false
},
@ -110,7 +104,6 @@
"id": "8f35404d-8237-4ff8-8774-9ad10ceee5c3",
"name": "charging",
"displayName": "Charging",
"displayNameEvent": "Charging changed",
"type": "bool",
"defaultValue": false
},
@ -118,7 +111,6 @@
"id": "179e6136-2ac1-4247-b457-f804e2212293",
"name": "phaseCount",
"displayName": "Number of connected phases",
"displayNameEvent": "Number of connected phases changed",
"type": "uint",
"minValue": 1,
"maxValue": 3,
@ -128,7 +120,6 @@
"id": "2027fbb6-c9d2-4a75-bdd0-a3ad3785cdc6",
"name": "currentPhase1",
"displayName": "Current phase 1",
"displayNameEvent": "Current phase 1 changed",
"type": "double",
"unit": "Ampere",
"defaultValue": 0.00
@ -137,7 +128,6 @@
"id": "1793f645-d7db-4e99-af92-3587aa3069f3",
"name": "currentPhase2",
"displayName": "Current phase 2",
"displayNameEvent": "Current phase 2 changed",
"type": "double",
"unit": "Ampere",
"defaultValue": 0.00
@ -146,7 +136,6 @@
"id": "feb8c5da-91a7-45f9-acc3-c1b61478c3d2",
"name": "currentPhase3",
"displayName": "Current phase 3",
"displayNameEvent": "Current phase 3 changed",
"type": "double",
"unit": "Ampere",
"defaultValue": 0.00
@ -155,7 +144,6 @@
"id": "b20a46ee-0f22-4096-a348-34e68e99e0be",
"name": "currentPower",
"displayName": "Current power consumption",
"displayNameEvent": "Current power consumtion changed",
"type": "double",
"unit": "Watt",
"defaultValue": 0.00
@ -164,7 +152,6 @@
"id": "80568c51-054c-4351-b9d2-e875fee4cc1f",
"name": "totalEnergyConsumed",
"displayName": "Total energy consumed",
"displayNameEvent": "Total energy consumption changed",
"type": "double",
"unit": "KiloWattHour",
"defaultValue": 0
@ -173,7 +160,6 @@
"id": "87c70567-794e-4af2-916c-b34cf864afcf",
"name": "sessionTime",
"displayName": "Session time",
"displayNameEvent": "Session time changed",
"type": "int",
"unit": "Minutes",
"defaultValue": 0
@ -182,7 +168,6 @@
"id": "b9b46920-55c1-4bfa-9200-acdc9c0a2471",
"name": "sessionEnergy",
"displayName": "Session energy",
"displayNameEvent": "Session energy changed",
"type": "double",
"unit": "KiloWattHour",
"defaultValue": 0
@ -191,7 +176,6 @@
"id": "56d31fd1-5cfb-42dd-8181-e6b0d0ca9c8a",
"name": "error",
"displayName": "Error ",
"displayNameEvent": "Error changed",
"type": "int",
"defaultValue": 0
},
@ -199,7 +183,6 @@
"id": "0e60b15d-2b0c-4672-960e-7c6ea67bf7ea",
"name": "maxPossibleChargingCurrent",
"displayName": "Maximum possible charging current",
"displayNameEvent": "Maximum possible charging current changed",
"type": "double",
"unit": "Ampere",
"defaultValue": 6.00
@ -208,7 +191,242 @@
"id": "48b62082-f286-433e-9cf8-2dcf6c0ea248",
"name": "userId",
"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",
"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)
# Generate modbus connection
MODBUS_CONNECTIONS += webasto-next-registers.json
MODBUS_TOOLS_CONFIG += VERBOSE
include(../modbus.pri)
SOURCES += \
integrationpluginwebasto.cpp \
webasto.cpp
webasto.cpp \
webastodiscovery.cpp
HEADERS += \
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