diff --git a/libnymea-modbus/modbustcpmaster.cpp b/libnymea-modbus/modbustcpmaster.cpp index 27ff535..fa66e24 100644 --- a/libnymea-modbus/modbustcpmaster.cpp +++ b/libnymea-modbus/modbustcpmaster.cpp @@ -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 &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 &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); } diff --git a/libnymea-modbus/modbustcpmaster.h b/libnymea-modbus/modbustcpmaster.h index 33f2780..968cc42 100644 --- a/libnymea-modbus/modbustcpmaster.h +++ b/libnymea-modbus/modbustcpmaster.h @@ -55,6 +55,8 @@ public: uint port() const; void setPort(uint port); + QString connectionUrl() const; + bool connected() const; int numberOfRetries() const; diff --git a/libnymea-modbus/tools/connectiontool/modbustcp.py b/libnymea-modbus/tools/connectiontool/modbustcp.py index 95a25bc..d01756a 100644 --- a/libnymea-modbus/tools/connectiontool/modbustcp.py +++ b/libnymea-modbus/tools/connectiontool/modbustcp.py @@ -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 diff --git a/webasto/README.md b/webasto/README.md index 2d5677e..dcfa38a 100644 --- a/webasto/README.md +++ b/webasto/README.md @@ -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/ diff --git a/webasto/integrationpluginwebasto.cpp b/webasto/integrationpluginwebasto.cpp index e041569..292cafc 100644 --- a/webasto/integrationpluginwebasto.cpp +++ b/webasto/integrationpluginwebasto.cpp @@ -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 #include +#include +#include #include #include @@ -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 ¶mTypeId, 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(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 &data) { Webasto *connection = static_cast(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; diff --git a/webasto/integrationpluginwebasto.h b/webasto/integrationpluginwebasto.h index eaed575..05da9a6 100644 --- a/webasto/integrationpluginwebasto.h +++ b/webasto/integrationpluginwebasto.h @@ -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 #include +#include +#include #include "webasto.h" +#include "webastonextmodbustcpconnection.h" +#include #include #include -#include 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 m_webastoConnections; + QHash m_asyncActions; + QHash m_webastoLiveConnections; + QHash m_webastoNextConnections; + QHash 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); diff --git a/webasto/integrationpluginwebasto.json b/webasto/integrationpluginwebasto.json index 01fba76..72c6dda 100644 --- a/webasto/integrationpluginwebasto.json +++ b/webasto/integrationpluginwebasto.json @@ -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 @@ } ] } - diff --git a/webasto/webasto-next-registers.json b/webasto/webasto-next-registers.json new file mode 100644 index 0000000..b7fa40c --- /dev/null +++ b/webasto/webasto-next-registers.json @@ -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" + } + ] +} diff --git a/webasto/webasto.pro b/webasto/webasto.pro index 45b6235..51fcca8 100644 --- a/webasto/webasto.pro +++ b/webasto/webasto.pro @@ -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 diff --git a/webasto/webastodiscovery.cpp b/webasto/webastodiscovery.cpp new file mode 100644 index 0000000..6a1d986 --- /dev/null +++ b/webasto/webastodiscovery.cpp @@ -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 . +* +* 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::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(); +} diff --git a/webasto/webastodiscovery.h b/webasto/webastodiscovery.h new file mode 100644 index 0000000..459ca03 --- /dev/null +++ b/webasto/webastodiscovery.h @@ -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 . +* +* 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 + +#include + +#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 results() const; + +signals: + void discoveryFinished(); + +private: + NetworkDeviceDiscovery *m_networkDeviceDiscovery = nullptr; + + QList m_connections; + + QList m_results; + + QDateTime m_startDateTime; + + void checkNetworkDevice(const NetworkDeviceInfo &networkDeviceInfo); + void cleanupConnection(WebastoNextModbusTcpConnection *connection); + + void finishDiscovery(); +}; + +#endif // WEBASTODISCOVERY_H