diff --git a/inro/crclookuptable.h b/inro/crclookuptable.h new file mode 100644 index 0000000..019597f --- /dev/null +++ b/inro/crclookuptable.h @@ -0,0 +1,42 @@ +#ifndef CRCLOOKUPTABLE_H +#define CRCLOOKUPTABLE_H +#include + +//CRC-8/NRSC-5 lookup table + +static const quint8 crc8LookupTable[256] = { + 0x00, 0x31, 0x62, 0x53, 0xc4, 0xf5, 0xa6, 0x97, + 0xb9, 0x88, 0xdb, 0xea, 0x7d, 0x4c, 0x1f, 0x2e, + 0x43, 0x72, 0x21, 0x10, 0x87, 0xb6, 0xe5, 0xd4, + 0xfa, 0xcb, 0x98, 0xa9, 0x3e, 0x0f, 0x5c, 0x6d, + 0x86, 0xb7, 0xe4, 0xd5, 0x42, 0x73, 0x20, 0x11, + 0x3f, 0x0e, 0x5d, 0x6c, 0xfb, 0xca, 0x99, 0xa8, + 0xc5, 0xf4, 0xa7, 0x96, 0x01, 0x30, 0x63, 0x52, + 0x7c, 0x4d, 0x1e, 0x2f, 0xb8, 0x89, 0xda, 0xeb, + 0x3d, 0x0c, 0x5f, 0x6e, 0xf9, 0xc8, 0x9b, 0xaa, + 0x84, 0xb5, 0xe6, 0xd7, 0x40, 0x71, 0x22, 0x13, + 0x7e, 0x4f, 0x1c, 0x2d, 0xba, 0x8b, 0xd8, 0xe9, + 0xc7, 0xf6, 0xa5, 0x94, 0x03, 0x32, 0x61, 0x50, + 0xbb, 0x8a, 0xd9, 0xe8, 0x7f, 0x4e, 0x1d, 0x2c, + 0x02, 0x33, 0x60, 0x51, 0xc6, 0xf7, 0xa4, 0x95, + 0xf8, 0xc9, 0x9a, 0xab, 0x3c, 0x0d, 0x5e, 0x6f, + 0x41, 0x70, 0x23, 0x12, 0x85, 0xb4, 0xe7, 0xd6, + 0x7a, 0x4b, 0x18, 0x29, 0xbe, 0x8f, 0xdc, 0xed, + 0xc3, 0xf2, 0xa1, 0x90, 0x07, 0x36, 0x65, 0x54, + 0x39, 0x08, 0x5b, 0x6a, 0xfd, 0xcc, 0x9f, 0xae, + 0x80, 0xb1, 0xe2, 0xd3, 0x44, 0x75, 0x26, 0x17, + 0xfc, 0xcd, 0x9e, 0xaf, 0x38, 0x09, 0x5a, 0x6b, + 0x45, 0x74, 0x27, 0x16, 0x81, 0xb0, 0xe3, 0xd2, + 0xbf, 0x8e, 0xdd, 0xec, 0x7b, 0x4a, 0x19, 0x28, + 0x06, 0x37, 0x64, 0x55, 0xc2, 0xf3, 0xa0, 0x91, + 0x47, 0x76, 0x25, 0x14, 0x83, 0xb2, 0xe1, 0xd0, + 0xfe, 0xcf, 0x9c, 0xad, 0x3a, 0x0b, 0x58, 0x69, + 0x04, 0x35, 0x66, 0x57, 0xc0, 0xf1, 0xa2, 0x93, + 0xbd, 0x8c, 0xdf, 0xee, 0x79, 0x48, 0x1b, 0x2a, + 0xc1, 0xf0, 0xa3, 0x92, 0x05, 0x34, 0x67, 0x56, + 0x78, 0x49, 0x1a, 0x2b, 0xbc, 0x8d, 0xde, 0xef, + 0x82, 0xb3, 0xe0, 0xd1, 0x46, 0x77, 0x24, 0x15, + 0x3b, 0x0a, 0x59, 0x68, 0xff, 0xce, 0x9d, 0xac +}; + +#endif // CRCLOOKUPTABLE_H diff --git a/inro/inro.pro b/inro/inro.pro index 8abdb2d..2bc5f38 100644 --- a/inro/inro.pro +++ b/inro/inro.pro @@ -7,9 +7,14 @@ MODBUS_TOOLS_CONFIG += VERBOSE include(../modbus.pri) HEADERS += \ + crclookuptable.h \ integrationplugininro.h \ - pantaboxdiscovery.h + pantabox.h \ + pantaboxdiscovery.h \ + pantaboxudpdiscovery.h SOURCES += \ integrationplugininro.cpp \ - pantaboxdiscovery.cpp + pantabox.cpp \ + pantaboxdiscovery.cpp \ + pantaboxudpdiscovery.cpp diff --git a/inro/integrationplugininro.cpp b/inro/integrationplugininro.cpp index 5eda2bc..64fe27f 100644 --- a/inro/integrationplugininro.cpp +++ b/inro/integrationplugininro.cpp @@ -31,7 +31,6 @@ #include "integrationplugininro.h" #include "plugininfo.h" -#include #include #include @@ -44,31 +43,24 @@ IntegrationPluginInro::IntegrationPluginInro() void IntegrationPluginInro::discoverThings(ThingDiscoveryInfo *info) { - - if (!hardwareManager()->networkDeviceDiscovery()->available()) { - qCWarning(dcInro()) << "The network discovery is not available on this platform."; - info->finish(Thing::ThingErrorUnsupportedFeature, QT_TR_NOOP("The network device discovery is not available.")); - return; - } - - PantaboxDiscovery *discovery = new PantaboxDiscovery(hardwareManager()->networkDeviceDiscovery(), info); + PantaboxDiscovery *discovery = new PantaboxDiscovery(info); connect(discovery, &PantaboxDiscovery::discoveryFinished, info, [this, info, discovery](){ foreach (const PantaboxDiscovery::Result &result, discovery->results()) { - QString title = QString("PANTABOX - %1").arg(result.serialNumber); - QString description = QString("%1 (%2)").arg(result.networkDeviceInfo.macAddress(), result.networkDeviceInfo.address().toString()); + QString title = QString("PANTABOX - %1").arg(result.deviceInfo.serialNumber); + QString description = QString("%1 (%2)").arg(result.deviceInfo.macAddress.toString(), result.deviceInfo.ipAddress.toString()); ThingDescriptor descriptor(pantaboxThingClassId, title, description); // Check if we already have set up this device - Things existingThings = myThings().filterByParam(pantaboxThingMacAddressParamTypeId, result.networkDeviceInfo.macAddress()); + Things existingThings = myThings().filterByParam(pantaboxThingSerialNumberParamTypeId, result.deviceInfo.serialNumber); if (existingThings.count() == 1) { - qCDebug(dcInro()) << "This PANTABOX already exists in the system:" << result.networkDeviceInfo; + qCDebug(dcInro()) << "This PANTABOX already exists in the system:" << result.deviceInfo.serialNumber << result.deviceInfo.ipAddress.toString(); descriptor.setThingId(existingThings.first()->id()); } ParamList params; - params << Param(pantaboxThingMacAddressParamTypeId, result.networkDeviceInfo.macAddress()); - params << Param(pantaboxThingSerialNumberParamTypeId, result.serialNumber); + params << Param(pantaboxThingMacAddressParamTypeId, result.deviceInfo.macAddress.toString()); + params << Param(pantaboxThingSerialNumberParamTypeId, result.deviceInfo.serialNumber); descriptor.setParams(params); info->addThingDescriptor(descriptor); } @@ -86,48 +78,21 @@ void IntegrationPluginInro::setupThing(ThingSetupInfo *info) if (m_connections.contains(thing)) { qCDebug(dcInro()) << "Reconfiguring existing thing" << thing->name(); - m_connections.take(thing)->deleteLater(); - - if (m_monitors.contains(thing)) { - hardwareManager()->networkDeviceDiscovery()->unregisterMonitor(m_monitors.take(thing)); - } + Pantabox *connection = m_connections.take(thing); + connection->modbusTcpMaster()->disconnectDevice(); + connection->deleteLater(); + thing->setStateValue(pantaboxConnectedStateTypeId, false); } - MacAddress macAddress = MacAddress(thing->paramValue(pantaboxThingMacAddressParamTypeId).toString()); - if (!macAddress.isValid()) { - qCWarning(dcInro()) << "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.")); + QString serialNumber = thing->paramValue(pantaboxThingSerialNumberParamTypeId).toString(); + + if (serialNumber.isEmpty()) { + qCWarning(dcInro()) << "Could not set up PANTABOX because the configured serial number is empty" << thing->params(); + info->finish(Thing::ThingErrorInvalidParameter, QT_TR_NOOP("The serial number is not known. Please reconfigure the thing.")); return; } - NetworkDeviceMonitor *monitor = hardwareManager()->networkDeviceDiscovery()->registerMonitor(macAddress); - m_monitors.insert(thing, monitor); - - connect(info, &ThingSetupInfo::aborted, monitor, [=](){ - if (m_monitors.contains(thing)) { - qCDebug(dcInro()) << "Unregistering monitor because setup has been aborted."; - hardwareManager()->networkDeviceDiscovery()->unregisterMonitor(m_monitors.take(thing)); - } - }); - - // Only make sure the connection is working in the initial setup, otherwise we let the monitor do the work - if (info->isInitialSetup()) { - // Continue with setup only if we know that the network device is reachable - if (monitor->reachable()) { - setupConnection(info); - } else { - // otherwise wait until we reach the networkdevice before setting up the device - qCDebug(dcInro()) << "Network device" << thing->name() << "is not reachable yet. Continue with the setup once reachable."; - connect(monitor, &NetworkDeviceMonitor::reachableChanged, info, [=](bool reachable){ - if (reachable) { - qCDebug(dcInro()) << "Network device" << thing->name() << "is now reachable. Continue with the setup..."; - setupConnection(info); - } - }); - } - } else { - setupConnection(info); - } + setupConnection(info); } void IntegrationPluginInro::postSetupThing(Thing *thing) @@ -136,7 +101,7 @@ void IntegrationPluginInro::postSetupThing(Thing *thing) if (!m_refreshTimer) { m_refreshTimer = hardwareManager()->pluginTimerManager()->registerTimer(2); connect(m_refreshTimer, &PluginTimer::timeout, this, [this] { - foreach (PantaboxModbusTcpConnection *connection, m_connections) { + foreach (Pantabox *connection, m_connections) { if (connection->reachable()) { connection->update(); } @@ -152,7 +117,7 @@ void IntegrationPluginInro::executeAction(ThingActionInfo *info) { if (info->thing()->thingClassId() == pantaboxThingClassId) { - PantaboxModbusTcpConnection *connection = m_connections.value(info->thing()); + Pantabox *connection = m_connections.value(info->thing()); if (!connection->reachable()) { qCWarning(dcInro()) << "Cannot execute action. The PANTABOX is not reachable"; @@ -162,27 +127,99 @@ void IntegrationPluginInro::executeAction(ThingActionInfo *info) if (info->action().actionTypeId() == pantaboxPowerActionTypeId) { bool power = info->action().paramValue(pantaboxPowerActionPowerParamTypeId).toBool(); - qCDebug(dcInro()) << "PANTABOX: Set power" << (power ? 1 : 0); - QModbusReply *reply = connection->setChargingEnabled(power ? 1 : 0); - if (!reply) { - qCWarning(dcInro()) << "Execute action failed because the reply could not be created."; - info->finish(Thing::ThingErrorHardwareFailure); - return; - } + // Play/pause charging session feature is available since Modbus Tcp version 1.2 (0x0001 0x0002) 0x10002 = 65538 + if (connection->modbusTcpVersion() < 65538) { + // When power is set by user, charging is going to stop or start depending on setting + qCDebug(dcInro()) << "Set power by user" << (power ? 1 : 0); + QModbusReply *reply = connection->setChargingEnabled(power ? 1 : 0); - connect(reply, &QModbusReply::finished, reply, &QModbusReply::deleteLater); - connect(reply, &QModbusReply::finished, info, [info, reply, power](){ - if (reply->error() == QModbusDevice::NoError) { - info->thing()->setStateValue(pantaboxPowerStateTypeId, power); - qCDebug(dcInro()) << "PANTABOX: Set power finished successfully"; - info->finish(Thing::ThingErrorNoError); - } else { - qCWarning(dcInro()) << "Error setting power:" << reply->error() << reply->errorString(); + if (!reply) { + qCWarning(dcInro()) << "Execute action failed because the reply could not be created."; info->finish(Thing::ThingErrorHardwareFailure); + return; } - }); - return; + + connect(reply, &QModbusReply::finished, reply, &QModbusReply::deleteLater); + connect(reply, &QModbusReply::finished, info, [info, reply, power](){ + if (reply->error() == QModbusDevice::NoError) { + info->thing()->setStateValue(pantaboxPowerStateTypeId, power); + qCDebug(dcInro()) << "Set power by user finished successfully"; + info->finish(Thing::ThingErrorNoError); + } else { + qCWarning(dcInro()) << "Error setting power by user:" << reply->error() << reply->errorString(); + info->finish(Thing::ThingErrorHardwareFailure); + } + }); + return; + + } else { + + // Modbus version >= 1.2 + + if (info->action().triggeredBy() == Action::TriggeredByUser) { + + // When power is set by user, charging is going to stop or start depending on setting + qCDebug(dcInro()) << "Set power by user" << (power ? 1 : 0); + QModbusReply *reply = connection->setChargingEnabled(power ? 1 : 0); + + if (!reply) { + qCWarning(dcInro()) << "Execute action failed because the reply could not be created."; + info->finish(Thing::ThingErrorHardwareFailure); + return; + } + + connect(reply, &QModbusReply::finished, reply, &QModbusReply::deleteLater); + connect(reply, &QModbusReply::finished, info, [info, reply, power](){ + if (reply->error() == QModbusDevice::NoError) { + info->thing()->setStateValue(pantaboxPowerStateTypeId, power); + qCDebug(dcInro()) << "Set power by user finished successfully"; + info->finish(Thing::ThingErrorNoError); + } else { + qCWarning(dcInro()) << "Error setting power by user:" << reply->error() << reply->errorString(); + info->finish(Thing::ThingErrorHardwareFailure); + } + }); + return; + + } else { + // When power is set to 0 by automatisnm, max charging current is set to 0 otherwise take the configured max charging current + qCDebug(dcInro()) << "Going to play/pause charging session"; + + quint16 chargingCurrent = power ? info->thing()->stateValue(pantaboxMaxChargingCurrentStateTypeId).toUInt() : 0; + QModbusReply *reply = connection->setMaxChargingCurrent(chargingCurrent); + + if (!reply) { + qCWarning(dcInro()) << "Execute action failed because the reply could not be created."; + info->finish(Thing::ThingErrorHardwareFailure); + return; + } + + connect(reply, &QModbusReply::finished, reply, &QModbusReply::deleteLater); + connect(reply, &QModbusReply::finished, info, [info, reply, power, connection](){ + if (reply->error() == QModbusDevice::NoError) { + qCDebug(dcInro()) << (power ? "Play" : "Pause") << "session by energy manager"; + info->finish(Thing::ThingErrorNoError); + if (power) { + // Make sure the charging is enabled, just in case + QModbusReply *reply = connection->setChargingEnabled(1); + connect(reply, &QModbusReply::finished, reply, &QModbusReply::deleteLater); + connect(reply, &QModbusReply::finished, info, [reply](){ + if (reply->error() != QModbusDevice::NoError) { + qCWarning(dcInro()) << "Error setting charging enabled:" << reply->error() << reply->errorString(); + } + }); + } + } else { + qCWarning(dcInro()) << "Error setting charging current:" << reply->error() << reply->errorString(); + info->finish(Thing::ThingErrorHardwareFailure); + } + + }); + + return; + } + } } if (info->action().actionTypeId() == pantaboxMaxChargingCurrentActionTypeId) { @@ -217,48 +254,61 @@ void IntegrationPluginInro::thingRemoved(Thing *thing) qCDebug(dcInro()) << "Thing removed" << thing->name(); if (m_connections.contains(thing)) { - PantaboxModbusTcpConnection *connection = m_connections.take(thing); + Pantabox *connection = m_connections.take(thing); connection->disconnectDevice(); connection->deleteLater(); } - // Unregister related hardware resources - if (m_monitors.contains(thing)) - hardwareManager()->networkDeviceDiscovery()->unregisterMonitor(m_monitors.take(thing)); + m_initReadRequired.remove(thing); if (myThings().isEmpty() && m_refreshTimer) { qCDebug(dcInro()) << "Stopping reconnect timer"; hardwareManager()->pluginTimerManager()->unregisterTimer(m_refreshTimer); m_refreshTimer = nullptr; } + + if (myThings().isEmpty() && m_udpDiscovery) { + qCDebug(dcInro()) << "Destroy UDP discovery since not needed any more"; + m_udpDiscovery->deleteLater(); + m_udpDiscovery = nullptr; + } } void IntegrationPluginInro::setupConnection(ThingSetupInfo *info) { + if (!m_udpDiscovery) + m_udpDiscovery = new PantaboxUdpDiscovery(this); + Thing *thing = info->thing(); - NetworkDeviceMonitor *monitor = m_monitors.value(thing); - PantaboxModbusTcpConnection *connection = new PantaboxModbusTcpConnection(monitor->networkDeviceInfo().address(), 502, 1, this); - connect(info, &ThingSetupInfo::aborted, connection, &PantaboxModbusTcpConnection::deleteLater); + Pantabox *connection = new Pantabox(QHostAddress(), 502, 1, this); + connect(info, &ThingSetupInfo::aborted, connection, &Pantabox::deleteLater); - // Monitor reachability - connect(monitor, &NetworkDeviceMonitor::reachableChanged, thing, [=](bool reachable){ - if (!thing->setupComplete()) + connect(m_udpDiscovery, &PantaboxUdpDiscovery::pantaboxDiscovered, connection, [connection, thing](const PantaboxUdpDiscovery::DeviceInfo &deviceInfo){ + QString serialNumber = thing->paramValue(pantaboxThingSerialNumberParamTypeId).toString(); + if (deviceInfo.serialNumber != serialNumber) return; - qCDebug(dcInro()) << "Network device monitor for" << thing->name() << (reachable ? "is now reachable" : "is not reachable any more" ); - if (reachable && !thing->stateValue("connected").toBool()) { - connection->modbusTcpMaster()->setHostAddress(monitor->networkDeviceInfo().address()); - connection->connectDevice(); - } else if (!reachable) { - // Note: We disable autoreconnect explicitly and we will - // connect the device once the monitor says it is reachable again - connection->disconnectDevice(); + connection->modbusTcpMaster()->setHostAddress(deviceInfo.ipAddress); + + if (!connection->reachable()) { + + if (connection->modbusTcpMaster()->connected()) { + qCDebug(dcInro()) << "Received discovery paket for" << thing->name() << + "which is not reachable but the TCP socket is still connected. Reconnecting the TCP socket on" << + deviceInfo.ipAddress.toString(); + connection->modbusTcpMaster()->reconnectDevice(); + } else { + qCDebug(dcInro()) << "Received discovery paket for" << thing->name() << + "which is not reachable and not connected. Start connecting to the PANTABOX on" << + deviceInfo.ipAddress.toString(); + connection->connectDevice(); + } } }); // Connection reachability - connect(connection, &PantaboxModbusTcpConnection::reachableChanged, thing, [thing, connection](bool reachable){ + connect(connection, &Pantabox::reachableChanged, thing, [thing, connection](bool reachable){ qCInfo(dcInro()) << "Reachable changed to" << reachable << "for" << thing; thing->setStateValue("connected", reachable); @@ -266,32 +316,39 @@ void IntegrationPluginInro::setupConnection(ThingSetupInfo *info) // Reset energy live values on disconnected thing->setStateValue(pantaboxCurrentPowerStateTypeId, 0); } else { - thing->setStateValue(pantaboxModbusTcpVersionStateTypeId, PantaboxDiscovery::modbusVersionToString(connection->modbusTcpVersion())); + connection->initialize(); } }); - connect(connection, &PantaboxModbusTcpConnection::updateFinished, thing, [thing, connection](){ + connect(connection, &Pantabox::initializationFinished, thing, [this, thing, connection](bool success){ + if (success) { + thing->setStateValue(pantaboxModbusTcpVersionStateTypeId, Pantabox::modbusVersionToString(connection->modbusTcpVersion())); + m_initReadRequired[thing] = true; + } + }); + + connect(connection, &Pantabox::updateFinished, thing, [this, thing, connection](){ qCDebug(dcInro()) << "Update finished for" << thing; qCDebug(dcInro()) << connection; QString chargingStateString; switch(connection->chargingState()) { - case PantaboxModbusTcpConnection::ChargingStateA: + case Pantabox::ChargingStateA: chargingStateString = "A"; break; - case PantaboxModbusTcpConnection::ChargingStateB: + case Pantabox::ChargingStateB: chargingStateString = "B"; break; - case PantaboxModbusTcpConnection::ChargingStateC: + case Pantabox::ChargingStateC: chargingStateString = "C"; break; - case PantaboxModbusTcpConnection::ChargingStateD: + case Pantabox::ChargingStateD: chargingStateString = "D"; break; - case PantaboxModbusTcpConnection::ChargingStateE: + case Pantabox::ChargingStateE: chargingStateString = "E"; break; - case PantaboxModbusTcpConnection::ChargingStateF: + case Pantabox::ChargingStateF: chargingStateString = "F"; break; } @@ -302,10 +359,10 @@ void IntegrationPluginInro::setupConnection(ThingSetupInfo *info) // C: connected, charging // D: ventilation required // E: F: fault/error - thing->setStateValue(pantaboxPluggedInStateTypeId, connection->chargingState() >= PantaboxModbusTcpConnection::ChargingStateB); - thing->setStateValue(pantaboxChargingStateTypeId, connection->chargingState() >= PantaboxModbusTcpConnection::ChargingStateC); + thing->setStateValue(pantaboxPluggedInStateTypeId, connection->chargingState() >= Pantabox::ChargingStateB); + thing->setStateValue(pantaboxChargingStateTypeId, connection->chargingState() >= Pantabox::ChargingStateC); thing->setStateValue(pantaboxCurrentPowerStateTypeId, connection->currentPower()); // W - thing->setStateValue(pantaboxTotalEnergyConsumedStateTypeId, connection->chargedEnergy() / 1000.0); // Wh + thing->setStateValue(pantaboxSessionEnergyStateTypeId, connection->chargedEnergy() / 1000.0); // Wh thing->setStateMaxValue(pantaboxMaxChargingCurrentActionTypeId, connection->maxPossibleChargingCurrent()); // Phase count is a setting, since we don't get the information from the device. @@ -316,14 +373,21 @@ void IntegrationPluginInro::setupConnection(ThingSetupInfo *info) thing->setStateValue(pantaboxPhaseCountStateTypeId, Electricity::getPhaseCount(phases)); thing->setStateValue(pantaboxUsedPhasesStateTypeId, thing->setting(pantaboxSettingsPhasesParamTypeId).toString()); + // Following states depend on the modbus TCP version, default they will be reset. + thing->setStateValue(pantaboxFirmwareVersionStateTypeId, connection->firmwareVersion()); + thing->setStateValue(pantaboxTotalEnergyConsumedStateTypeId, connection->absoluteEnergy() / 1000.0); // Wh + + // Sync states only right after the connection + if (m_initReadRequired.value(thing, false)) { + qCDebug(dcInro()) << "Set initial charging current and charging enabled values."; + m_initReadRequired.remove(thing); + if (connection->maxChargingCurrent() > 0) { + thing->setStateValue(pantaboxMaxChargingCurrentStateTypeId, connection->maxChargingCurrent()); + } + thing->setStateValue(pantaboxPowerStateTypeId, connection->chargingEnabled()); + } }); m_connections.insert(thing, connection); info->finish(Thing::ThingErrorNoError); - - qCDebug(dcInro()) << "Setting up PANTABOX finished successfully" << monitor->networkDeviceInfo().address().toString(); - - // Connect reight the way if the monitor indicates reachable, otherwise the connect will handle the connect later - if (monitor->reachable()) - connection->connectDevice(); } diff --git a/inro/integrationplugininro.h b/inro/integrationplugininro.h index 3267bdb..36e5ac0 100644 --- a/inro/integrationplugininro.h +++ b/inro/integrationplugininro.h @@ -36,7 +36,8 @@ #include #include "extern-plugininfo.h" -#include "pantaboxmodbustcpconnection.h" +#include "pantabox.h" +#include "pantaboxudpdiscovery.h" class IntegrationPluginInro: public IntegrationPlugin { @@ -56,8 +57,10 @@ public: private: PluginTimer *m_refreshTimer = nullptr; - QHash m_connections; - QHash m_monitors; + QHash m_connections; + QHash m_initReadRequired; + + PantaboxUdpDiscovery *m_udpDiscovery = nullptr; void setupConnection(ThingSetupInfo *info); }; diff --git a/inro/integrationplugininro.json b/inro/integrationplugininro.json index d5e1962..0176106 100644 --- a/inro/integrationplugininro.json +++ b/inro/integrationplugininro.json @@ -146,6 +146,13 @@ "displayName": "Modbus TCP version", "type": "QString", "defaultValue": "" + }, + { + "id": "8c92890f-c9fb-44dd-8665-f1502fc90912", + "name": "firmwareVersion", + "displayName": "Firmware version", + "type": "QString", + "defaultValue": "" } ] } diff --git a/inro/pantabox-registers.json b/inro/pantabox-registers.json index 05ec69f..6e59fb9 100644 --- a/inro/pantabox-registers.json +++ b/inro/pantabox-registers.json @@ -2,6 +2,7 @@ "className": "Pantabox", "protocol": "TCP", "endianness": "LittleEndian", + "stringEndianness": "LittleEndian", "errorLimitUntilNotReachable": 2, "checkReachableRegister": "chargingState", "blocks": [ ], @@ -59,6 +60,34 @@ "defaultValue": "0", "access": "RO" }, + { + "id": "vendorName", + "address": 260, + "size": 2, + "type": "string", + "registerType": "inputRegister", + "description": "Name of vendor", + "access": "RO" + }, + { + "id": "productName", + "address": 262, + "size": 4, + "type": "string", + "registerType": "inputRegister", + "description": "Name of product", + "access": "RO" + }, + { + "id": "firmwareVersion", + "address": 266, + "size": 16, + "type": "string", + "registerType": "inputRegister", + "description": "Firmware version", + "readSchedule": "update", + "access": "RO" + }, { "id": "chargingState", "address": 512, @@ -119,6 +148,18 @@ "defaultValue": "0", "access": "RO" }, + { + "id": "absoluteEnergy", + "address": 519, + "size": 2, + "type": "uint32", + "registerType": "inputRegister", + "description": "Absolute charged energy", + "unit": "Wh", + "readSchedule": "update", + "defaultValue": "0", + "access": "RO" + }, { "id": "chargingEnabled", "address": 768, diff --git a/inro/pantabox.cpp b/inro/pantabox.cpp new file mode 100644 index 0000000..9195c00 --- /dev/null +++ b/inro/pantabox.cpp @@ -0,0 +1,403 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* Copyright 2013 - 2024, 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 "pantabox.h" +#include "pantaboxmodbustcpconnection.cpp" + +Pantabox::Pantabox(const QHostAddress &hostAddress, uint port, quint16 slaveId, QObject *parent) : + PantaboxModbusTcpConnection(hostAddress, port, slaveId, parent) +{ + +} + +QString Pantabox::modbusVersionToString(quint32 value) +{ + quint16 modbusVersionMinor = (value >> 16) & 0xffff; + quint16 modbusVersionMajor = value & 0xffff; + return QString("%1.%2").arg(modbusVersionMajor).arg(modbusVersionMinor); +} + +bool Pantabox::update() { + if (!m_modbusTcpMaster->connected()) + return false; + + if (!m_pendingUpdateReplies.isEmpty()) { + qCDebug(dcPantaboxModbusTcpConnection()) << "Tried to update but there are still some update replies pending. Waiting for them to be finished..."; + return true; + } + + QModbusReply *reply = nullptr; + + // Read Charging state + qCDebug(dcPantaboxModbusTcpConnection()) << "--> Read \"Charging state\" register:" << 512 << "size:" << 1; + reply = readChargingState(); + if (!reply) { + qCWarning(dcPantaboxModbusTcpConnection()) << "Error occurred while reading \"Charging state\" registers from" << m_modbusTcpMaster->hostAddress().toString() << m_modbusTcpMaster->errorString(); + return false; + } + + if (reply->isFinished()) { + reply->deleteLater(); // Broadcast reply returns immediatly + return false; + } + + m_pendingUpdateReplies.append(reply); + connect(reply, &QModbusReply::finished, reply, &QModbusReply::deleteLater); + connect(reply, &QModbusReply::finished, this, [this, reply](){ + m_pendingUpdateReplies.removeAll(reply); + handleModbusError(reply->error()); + if (reply->error() != QModbusDevice::NoError) { + verifyUpdateFinished(); + return; + } + + const QModbusDataUnit unit = reply->result(); + processChargingStateRegisterValues(unit.values()); + verifyUpdateFinished(); + }); + + connect(reply, &QModbusReply::errorOccurred, this, [this, reply] (QModbusDevice::Error error){ + QModbusResponse response = reply->rawResult(); + if (reply->error() == QModbusDevice::ProtocolError && response.isException()) { + qCWarning(dcPantaboxModbusTcpConnection()) << "Modbus reply error occurred while reading \"Charging state\" registers from" << m_modbusTcpMaster->hostAddress().toString() << error << reply->errorString() << ModbusDataUtils::exceptionCodeToString(response.exceptionCode()); + } else { + qCWarning(dcPantaboxModbusTcpConnection()) << "Modbus reply error occurred while reading \"Charging state\" registers from" << m_modbusTcpMaster->hostAddress().toString() << error << reply->errorString(); + } + }); + + // Read Current charging power + qCDebug(dcPantaboxModbusTcpConnection()) << "--> Read \"Current charging power\" register:" << 513 << "size:" << 2; + reply = readCurrentPower(); + if (!reply) { + qCWarning(dcPantaboxModbusTcpConnection()) << "Error occurred while reading \"Current charging power\" registers from" << m_modbusTcpMaster->hostAddress().toString() << m_modbusTcpMaster->errorString(); + return false; + } + + if (reply->isFinished()) { + reply->deleteLater(); // Broadcast reply returns immediatly + return false; + } + + m_pendingUpdateReplies.append(reply); + connect(reply, &QModbusReply::finished, reply, &QModbusReply::deleteLater); + connect(reply, &QModbusReply::finished, this, [this, reply](){ + m_pendingUpdateReplies.removeAll(reply); + handleModbusError(reply->error()); + if (reply->error() != QModbusDevice::NoError) { + verifyUpdateFinished(); + return; + } + + const QModbusDataUnit unit = reply->result(); + processCurrentPowerRegisterValues(unit.values()); + verifyUpdateFinished(); + }); + + connect(reply, &QModbusReply::errorOccurred, this, [this, reply] (QModbusDevice::Error error){ + QModbusResponse response = reply->rawResult(); + if (reply->error() == QModbusDevice::ProtocolError && response.isException()) { + qCWarning(dcPantaboxModbusTcpConnection()) << "Modbus reply error occurred while reading \"Current charging power\" registers from" << m_modbusTcpMaster->hostAddress().toString() << error << reply->errorString() << ModbusDataUtils::exceptionCodeToString(response.exceptionCode()); + } else { + qCWarning(dcPantaboxModbusTcpConnection()) << "Modbus reply error occurred while reading \"Current charging power\" registers from" << m_modbusTcpMaster->hostAddress().toString() << error << reply->errorString(); + } + }); + + // Read Charged energy + qCDebug(dcPantaboxModbusTcpConnection()) << "--> Read \"Charged energy\" register:" << 515 << "size:" << 2; + reply = readChargedEnergy(); + if (!reply) { + qCWarning(dcPantaboxModbusTcpConnection()) << "Error occurred while reading \"Charged energy\" registers from" << m_modbusTcpMaster->hostAddress().toString() << m_modbusTcpMaster->errorString(); + return false; + } + + if (reply->isFinished()) { + reply->deleteLater(); // Broadcast reply returns immediatly + return false; + } + + m_pendingUpdateReplies.append(reply); + connect(reply, &QModbusReply::finished, reply, &QModbusReply::deleteLater); + connect(reply, &QModbusReply::finished, this, [this, reply](){ + m_pendingUpdateReplies.removeAll(reply); + handleModbusError(reply->error()); + if (reply->error() != QModbusDevice::NoError) { + verifyUpdateFinished(); + return; + } + + const QModbusDataUnit unit = reply->result(); + processChargedEnergyRegisterValues(unit.values()); + verifyUpdateFinished(); + }); + + connect(reply, &QModbusReply::errorOccurred, this, [this, reply] (QModbusDevice::Error error){ + QModbusResponse response = reply->rawResult(); + if (reply->error() == QModbusDevice::ProtocolError && response.isException()) { + qCWarning(dcPantaboxModbusTcpConnection()) << "Modbus reply error occurred while reading \"Charged energy\" registers from" << m_modbusTcpMaster->hostAddress().toString() << error << reply->errorString() << ModbusDataUtils::exceptionCodeToString(response.exceptionCode()); + } else { + qCWarning(dcPantaboxModbusTcpConnection()) << "Modbus reply error occurred while reading \"Charged energy\" registers from" << m_modbusTcpMaster->hostAddress().toString() << error << reply->errorString(); + } + }); + + // Read Maximal possible charging current (adapter) + qCDebug(dcPantaboxModbusTcpConnection()) << "--> Read \"Maximal possible charging current (adapter)\" register:" << 517 << "size:" << 1; + reply = readMaxPossibleChargingCurrent(); + if (!reply) { + qCWarning(dcPantaboxModbusTcpConnection()) << "Error occurred while reading \"Maximal possible charging current (adapter)\" registers from" << m_modbusTcpMaster->hostAddress().toString() << m_modbusTcpMaster->errorString(); + return false; + } + + if (reply->isFinished()) { + reply->deleteLater(); // Broadcast reply returns immediatly + return false; + } + + m_pendingUpdateReplies.append(reply); + connect(reply, &QModbusReply::finished, reply, &QModbusReply::deleteLater); + connect(reply, &QModbusReply::finished, this, [this, reply](){ + m_pendingUpdateReplies.removeAll(reply); + handleModbusError(reply->error()); + if (reply->error() != QModbusDevice::NoError) { + verifyUpdateFinished(); + return; + } + + const QModbusDataUnit unit = reply->result(); + processMaxPossibleChargingCurrentRegisterValues(unit.values()); + verifyUpdateFinished(); + }); + + connect(reply, &QModbusReply::errorOccurred, this, [this, reply] (QModbusDevice::Error error){ + QModbusResponse response = reply->rawResult(); + if (reply->error() == QModbusDevice::ProtocolError && response.isException()) { + qCWarning(dcPantaboxModbusTcpConnection()) << "Modbus reply error occurred while reading \"Maximal possible charging current (adapter)\" registers from" << m_modbusTcpMaster->hostAddress().toString() << error << reply->errorString() << ModbusDataUtils::exceptionCodeToString(response.exceptionCode()); + } else { + qCWarning(dcPantaboxModbusTcpConnection()) << "Modbus reply error occurred while reading \"Maximal possible charging current (adapter)\" registers from" << m_modbusTcpMaster->hostAddress().toString() << error << reply->errorString(); + } + }); + + // Read Actual charging current + qCDebug(dcPantaboxModbusTcpConnection()) << "--> Read \"Actual charging current\" register:" << 518 << "size:" << 1; + reply = readChargingCurrent(); + if (!reply) { + qCWarning(dcPantaboxModbusTcpConnection()) << "Error occurred while reading \"Actual charging current\" registers from" << m_modbusTcpMaster->hostAddress().toString() << m_modbusTcpMaster->errorString(); + return false; + } + + if (reply->isFinished()) { + reply->deleteLater(); // Broadcast reply returns immediatly + return false; + } + + m_pendingUpdateReplies.append(reply); + connect(reply, &QModbusReply::finished, reply, &QModbusReply::deleteLater); + connect(reply, &QModbusReply::finished, this, [this, reply](){ + m_pendingUpdateReplies.removeAll(reply); + handleModbusError(reply->error()); + if (reply->error() != QModbusDevice::NoError) { + verifyUpdateFinished(); + return; + } + + const QModbusDataUnit unit = reply->result(); + processChargingCurrentRegisterValues(unit.values()); + verifyUpdateFinished(); + }); + + connect(reply, &QModbusReply::errorOccurred, this, [this, reply] (QModbusDevice::Error error){ + QModbusResponse response = reply->rawResult(); + if (reply->error() == QModbusDevice::ProtocolError && response.isException()) { + qCWarning(dcPantaboxModbusTcpConnection()) << "Modbus reply error occurred while reading \"Actual charging current\" registers from" << m_modbusTcpMaster->hostAddress().toString() << error << reply->errorString() << ModbusDataUtils::exceptionCodeToString(response.exceptionCode()); + } else { + qCWarning(dcPantaboxModbusTcpConnection()) << "Modbus reply error occurred while reading \"Actual charging current\" registers from" << m_modbusTcpMaster->hostAddress().toString() << error << reply->errorString(); + } + }); + + // Read Charging enabled (1) / disabled (0) + qCDebug(dcPantaboxModbusTcpConnection()) << "--> Read \"Charging enabled (1) / disabled (0)\" register:" << 768 << "size:" << 1; + reply = readChargingEnabled(); + if (!reply) { + qCWarning(dcPantaboxModbusTcpConnection()) << "Error occurred while reading \"Charging enabled (1) / disabled (0)\" registers from" << m_modbusTcpMaster->hostAddress().toString() << m_modbusTcpMaster->errorString(); + return false; + } + + if (reply->isFinished()) { + reply->deleteLater(); // Broadcast reply returns immediatly + return false; + } + + m_pendingUpdateReplies.append(reply); + connect(reply, &QModbusReply::finished, reply, &QModbusReply::deleteLater); + connect(reply, &QModbusReply::finished, this, [this, reply](){ + m_pendingUpdateReplies.removeAll(reply); + handleModbusError(reply->error()); + if (reply->error() != QModbusDevice::NoError) { + verifyUpdateFinished(); + return; + } + + const QModbusDataUnit unit = reply->result(); + processChargingEnabledRegisterValues(unit.values()); + verifyUpdateFinished(); + }); + + connect(reply, &QModbusReply::errorOccurred, this, [this, reply] (QModbusDevice::Error error){ + QModbusResponse response = reply->rawResult(); + if (reply->error() == QModbusDevice::ProtocolError && response.isException()) { + qCWarning(dcPantaboxModbusTcpConnection()) << "Modbus reply error occurred while reading \"Charging enabled (1) / disabled (0)\" registers from" << m_modbusTcpMaster->hostAddress().toString() << error << reply->errorString() << ModbusDataUtils::exceptionCodeToString(response.exceptionCode()); + } else { + qCWarning(dcPantaboxModbusTcpConnection()) << "Modbus reply error occurred while reading \"Charging enabled (1) / disabled (0)\" registers from" << m_modbusTcpMaster->hostAddress().toString() << error << reply->errorString(); + } + }); + + // Read Max charging current + qCDebug(dcPantaboxModbusTcpConnection()) << "--> Read \"Max charging current\" register:" << 769 << "size:" << 1; + reply = readMaxChargingCurrent(); + if (!reply) { + qCWarning(dcPantaboxModbusTcpConnection()) << "Error occurred while reading \"Max charging current\" registers from" << m_modbusTcpMaster->hostAddress().toString() << m_modbusTcpMaster->errorString(); + return false; + } + + if (reply->isFinished()) { + reply->deleteLater(); // Broadcast reply returns immediatly + return false; + } + + m_pendingUpdateReplies.append(reply); + connect(reply, &QModbusReply::finished, reply, &QModbusReply::deleteLater); + connect(reply, &QModbusReply::finished, this, [this, reply](){ + m_pendingUpdateReplies.removeAll(reply); + handleModbusError(reply->error()); + if (reply->error() != QModbusDevice::NoError) { + verifyUpdateFinished(); + return; + } + + const QModbusDataUnit unit = reply->result(); + processMaxChargingCurrentRegisterValues(unit.values()); + verifyUpdateFinished(); + }); + + connect(reply, &QModbusReply::errorOccurred, this, [this, reply] (QModbusDevice::Error error){ + QModbusResponse response = reply->rawResult(); + if (reply->error() == QModbusDevice::ProtocolError && response.isException()) { + qCWarning(dcPantaboxModbusTcpConnection()) << "Modbus reply error occurred while reading \"Max charging current\" registers from" << m_modbusTcpMaster->hostAddress().toString() << error << reply->errorString() << ModbusDataUtils::exceptionCodeToString(response.exceptionCode()); + } else { + qCWarning(dcPantaboxModbusTcpConnection()) << "Modbus reply error occurred while reading \"Max charging current\" registers from" << m_modbusTcpMaster->hostAddress().toString() << error << reply->errorString(); + } + }); + + + // Following Modbus registers depend on the modbus TCP protocol version and require compatibility checks + + // Firmware version registers are available since modbus TCP version 1.1 (0x0001 0x0001) 0x10001 = 65537 + if (m_modbusTcpVersion >= 65537) { + + // Read Firmware version + qCDebug(dcPantaboxModbusTcpConnection()) << "--> Read \"Firmware version\" register:" << 266 << "size:" << 16; + reply = readFirmwareVersion(); + if (!reply) { + qCWarning(dcPantaboxModbusTcpConnection()) << "Error occurred while reading \"Firmware version\" registers from" << m_modbusTcpMaster->hostAddress().toString() << m_modbusTcpMaster->errorString(); + return false; + } + + if (reply->isFinished()) { + reply->deleteLater(); // Broadcast reply returns immediatly + return false; + } + + m_pendingUpdateReplies.append(reply); + connect(reply, &QModbusReply::finished, reply, &QModbusReply::deleteLater); + connect(reply, &QModbusReply::finished, this, [this, reply](){ + m_pendingUpdateReplies.removeAll(reply); + handleModbusError(reply->error()); + if (reply->error() != QModbusDevice::NoError) { + verifyUpdateFinished(); + return; + } + + const QModbusDataUnit unit = reply->result(); + processFirmwareVersionRegisterValues(unit.values()); + verifyUpdateFinished(); + }); + + connect(reply, &QModbusReply::errorOccurred, this, [this, reply] (QModbusDevice::Error error){ + QModbusResponse response = reply->rawResult(); + if (reply->error() == QModbusDevice::ProtocolError && response.isException()) { + qCWarning(dcPantaboxModbusTcpConnection()) << "Modbus reply error occurred while reading \"Firmware version\" registers from" << m_modbusTcpMaster->hostAddress().toString() << error << reply->errorString() << ModbusDataUtils::exceptionCodeToString(response.exceptionCode()); + } else { + qCWarning(dcPantaboxModbusTcpConnection()) << "Modbus reply error occurred while reading \"Firmware version\" registers from" << m_modbusTcpMaster->hostAddress().toString() << error << reply->errorString(); + } + }); + } + + // Absolute charged energy is available since modbus TCP version 1.2 (0x0001 0x0002) 0x10002 = 65538 + if (m_modbusTcpVersion >= 65538) { + + // Read Absolute charged energy + qCDebug(dcPantaboxModbusTcpConnection()) << "--> Read \"Absolute charged energy\" register:" << 519 << "size:" << 2; + reply = readAbsoluteEnergy(); + if (!reply) { + qCWarning(dcPantaboxModbusTcpConnection()) << "Error occurred while reading \"Absolute charged energy\" registers from" << m_modbusTcpMaster->hostAddress().toString() << m_modbusTcpMaster->errorString(); + return false; + } + + if (reply->isFinished()) { + reply->deleteLater(); // Broadcast reply returns immediatly + return false; + } + + m_pendingUpdateReplies.append(reply); + connect(reply, &QModbusReply::finished, reply, &QModbusReply::deleteLater); + connect(reply, &QModbusReply::finished, this, [this, reply](){ + m_pendingUpdateReplies.removeAll(reply); + handleModbusError(reply->error()); + if (reply->error() != QModbusDevice::NoError) { + verifyUpdateFinished(); + return; + } + + const QModbusDataUnit unit = reply->result(); + processAbsoluteEnergyRegisterValues(unit.values()); + verifyUpdateFinished(); + }); + + connect(reply, &QModbusReply::errorOccurred, this, [this, reply] (QModbusDevice::Error error){ + QModbusResponse response = reply->rawResult(); + if (reply->error() == QModbusDevice::ProtocolError && response.isException()) { + qCWarning(dcPantaboxModbusTcpConnection()) << "Modbus reply error occurred while reading \"Absolute charged energy\" registers from" << m_modbusTcpMaster->hostAddress().toString() << error << reply->errorString() << ModbusDataUtils::exceptionCodeToString(response.exceptionCode()); + } else { + qCWarning(dcPantaboxModbusTcpConnection()) << "Modbus reply error occurred while reading \"Absolute charged energy\" registers from" << m_modbusTcpMaster->hostAddress().toString() << error << reply->errorString(); + } + }); + } + + return true; +} diff --git a/inro/pantabox.h b/inro/pantabox.h new file mode 100644 index 0000000..a9fe6ee --- /dev/null +++ b/inro/pantabox.h @@ -0,0 +1,49 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* Copyright 2013 - 2024, 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 PANTABOX_H +#define PANTABOX_H + +#include "pantaboxmodbustcpconnection.h" + +class Pantabox : public PantaboxModbusTcpConnection +{ + Q_OBJECT +public: + explicit Pantabox(const QHostAddress &hostAddress, uint port, quint16 slaveId, QObject *parent = nullptr); + explicit Pantabox(ModbusTcpMaster *modbusTcpMaster, quint16 slaveId, QObject *parent = nullptr); + ~Pantabox() override = default ; + + static QString modbusVersionToString(quint32 value); + + bool update() override; +}; + +#endif // PANTABOX_H diff --git a/inro/pantaboxdiscovery.cpp b/inro/pantaboxdiscovery.cpp index 7c6ccad..0ccbf50 100644 --- a/inro/pantaboxdiscovery.cpp +++ b/inro/pantaboxdiscovery.cpp @@ -31,9 +31,8 @@ #include "pantaboxdiscovery.h" #include "extern-plugininfo.h" -PantaboxDiscovery::PantaboxDiscovery(NetworkDeviceDiscovery *networkDeviceDiscovery, QObject *parent) - : QObject{parent}, - m_networkDeviceDiscovery{networkDeviceDiscovery} +PantaboxDiscovery::PantaboxDiscovery(QObject *parent) + : QObject{parent} { } @@ -43,36 +42,30 @@ QList PantaboxDiscovery::results() const return m_results; } -QString PantaboxDiscovery::modbusVersionToString(quint32 value) -{ - quint16 modbusVersionMinor = (value >> 8) & 0xffff; - quint16 modbusVersionMajor = value & 0xffff; - return QString("%1.%2").arg(modbusVersionMajor).arg(modbusVersionMinor); -} - void PantaboxDiscovery::startDiscovery() { qCInfo(dcInro()) << "Discovery: Start searching for PANTABOX wallboxes in the network..."; m_startDateTime = QDateTime::currentDateTime(); - NetworkDeviceDiscoveryReply *discoveryReply = m_networkDeviceDiscovery->discover(); - connect(discoveryReply, &NetworkDeviceDiscoveryReply::networkDeviceInfoAdded, this, &PantaboxDiscovery::checkNetworkDevice); - connect(discoveryReply, &NetworkDeviceDiscoveryReply::finished, discoveryReply, &NetworkDeviceDiscoveryReply::deleteLater); - connect(discoveryReply, &NetworkDeviceDiscoveryReply::finished, this, [=](){ - // Finish with some delay so the last added network device information objects still can be checked. - QTimer::singleShot(3000, this, [this](){ - qCDebug(dcInro()) << "Discovery: Grace period timer triggered."; - finishDiscovery(); - }); - }); + m_discovery = new PantaboxUdpDiscovery(this); + connect(m_discovery, &PantaboxUdpDiscovery::pantaboxDiscovered, this, &PantaboxDiscovery::checkNetworkDevice); + + connect(&m_discoveryTimer, &QTimer::timeout, this, &PantaboxDiscovery::finishDiscovery); + m_discoveryTimer.setSingleShot(true); + m_discoveryTimer.start(10000); } -void PantaboxDiscovery::checkNetworkDevice(const NetworkDeviceInfo &networkDeviceInfo) +void PantaboxDiscovery::checkNetworkDevice(const PantaboxUdpDiscovery::DeviceInfo &deviceInfo) { - PantaboxModbusTcpConnection *connection = new PantaboxModbusTcpConnection(networkDeviceInfo.address(), m_port, m_modbusAddress, this); + if (m_alreadyCheckedHosts.contains(deviceInfo.ipAddress)) + return; + + m_alreadyCheckedHosts.append(deviceInfo.ipAddress); + + Pantabox *connection = new Pantabox(deviceInfo.ipAddress, m_port, m_modbusAddress, this); m_connections.append(connection); - connect(connection, &PantaboxModbusTcpConnection::reachableChanged, this, [=](bool reachable){ + connect(connection, &Pantabox::reachableChanged, this, [=](bool reachable){ if (!reachable) { // Disconnected ... done with this connection cleanupConnection(connection); @@ -80,35 +73,61 @@ void PantaboxDiscovery::checkNetworkDevice(const NetworkDeviceInfo &networkDevic } // Modbus TCP connected...ok, let's try to initialize it! - connect(connection, &PantaboxModbusTcpConnection::initializationFinished, this, [=](bool success){ + connect(connection, &Pantabox::initializationFinished, this, [=](bool success){ if (!success) { - qCDebug(dcInro()) << "Discovery: Initialization failed on" << networkDeviceInfo.address().toString() << "Continue..."; + qCDebug(dcInro()) << "Discovery: Initialization failed on" << deviceInfo.ipAddress.toString() << "Continue..."; cleanupConnection(connection); return; } - // FIXME: find a better way to discover the device besides a valid init - qCDebug(dcInro()) << "Discovery: Connection initialized successfully" << connection->serialNumber(); + // Vendor and product name registers are available since modbus TCP version 1.1 (0x0001 0x0001) 0x10001 = 65537 + if (connection->modbusTcpVersion() >= 65537) { - Result result; - result.serialNumber = QString::number(connection->serialNumber(), 16).toUpper(); - result.modbusTcpVersion = modbusVersionToString(connection->modbusTcpVersion()); - result.networkDeviceInfo = networkDeviceInfo; - m_results.append(result); + QModbusReply *reply = connection->readProductName(); + if (!reply) { + cleanupConnection(connection); + return; + } - qCInfo(dcInro()) << "Discovery: --> Found" - << "Serial number:" << result.serialNumber - << "(" << connection->serialNumber() << ")" - << "ModbusTCP version:" << result.modbusTcpVersion - << result.networkDeviceInfo; + if (reply->isFinished()) { + reply->deleteLater(); // Broadcast reply returns immediatly + cleanupConnection(connection); + return; + } - // Done with this connection - cleanupConnection(connection); + connect(reply, &QModbusReply::finished, reply, &QModbusReply::deleteLater); + connect(reply, &QModbusReply::finished, this, [this, reply, connection, deviceInfo](){ + if (reply->error() != QModbusDevice::NoError) { + qCDebug(dcInro()) << "Discovery: Error reading product name error on" << deviceInfo.ipAddress.toString() << "Continue..."; + cleanupConnection(connection); + return; + } + + const QModbusDataUnit unit = reply->result(); + if (unit.values().size() == 4) { + QString receivedProductName = ModbusDataUtils::convertToString(unit.values(), connection->stringEndianness()); + if (receivedProductName.toUpper().contains("PANTABOX")) { + addResult(connection, deviceInfo); + } else { + qCDebug(dcInro()) << "Discovery: Invalid product name " << receivedProductName + << "on" << deviceInfo.ipAddress.toString() << "Continue..."; + cleanupConnection(connection); + } + } else { + qCDebug(dcInro()) << "Discovery: Reading from \"Name of product\" registers" << 262 << "size:" << 4 << "returned different size than requested. Ignoring incomplete data" << unit.values(); + cleanupConnection(connection); + } + }); + } else { + qCDebug(dcInro()) << "Discovery: Adding connection to results even tough the result is not precise due to modbus TCP protocol version:" + << connection->modbusTcpVersion() << Pantabox::modbusVersionToString(connection->modbusTcpVersion()); + addResult(connection, deviceInfo); + } }); // Initializing... if (!connection->initialize()) { - qCDebug(dcInro()) << "Discovery: Unable to initialize connection on" << networkDeviceInfo.address().toString() << "Continue..."; + qCDebug(dcInro()) << "Discovery: Unable to initialize connection on" << deviceInfo.ipAddress.toString() << "Continue..."; cleanupConnection(connection); } }); @@ -116,14 +135,14 @@ void PantaboxDiscovery::checkNetworkDevice(const NetworkDeviceInfo &networkDevic // If we get any error...skip this host... connect(connection->modbusTcpMaster(), &ModbusTcpMaster::connectionErrorOccurred, this, [=](QModbusDevice::Error error){ if (error != QModbusDevice::NoError) { - qCDebug(dcInro()) << "Discovery: Connection error on" << networkDeviceInfo.address().toString() << "Continue..."; + qCDebug(dcInro()) << "Discovery: Connection error on" << deviceInfo.ipAddress.toString() << "Continue..."; cleanupConnection(connection); } }); // If check reachability failed...skip this host... - connect(connection, &PantaboxModbusTcpConnection::checkReachabilityFailed, this, [=](){ - qCDebug(dcInro()) << "Discovery: Check reachability failed on" << networkDeviceInfo.address().toString() << "Continue..."; + connect(connection, &Pantabox::checkReachabilityFailed, this, [=](){ + qCDebug(dcInro()) << "Discovery: Check reachability failed on" << deviceInfo.ipAddress.toString() << "Continue..."; cleanupConnection(connection); }); @@ -131,7 +150,7 @@ void PantaboxDiscovery::checkNetworkDevice(const NetworkDeviceInfo &networkDevic connection->connectDevice(); } -void PantaboxDiscovery::cleanupConnection(PantaboxModbusTcpConnection *connection) +void PantaboxDiscovery::cleanupConnection(Pantabox *connection) { m_connections.removeAll(connection); connection->disconnectDevice(); @@ -142,11 +161,43 @@ void PantaboxDiscovery::finishDiscovery() { qint64 durationMilliSeconds = QDateTime::currentMSecsSinceEpoch() - m_startDateTime.toMSecsSinceEpoch(); + m_discovery->deleteLater(); + m_discovery = nullptr; + + m_alreadyCheckedHosts.clear(); + // Cleanup any leftovers...we don't care any more - foreach (PantaboxModbusTcpConnection *connection, m_connections) + foreach (Pantabox *connection, m_connections) cleanupConnection(connection); qCInfo(dcInro()) << "Discovery: Finished the discovery process. Found" << m_results.count() << "PANTABOXE wallboxes in" << QTime::fromMSecsSinceStartOfDay(durationMilliSeconds).toString("mm:ss.zzz"); + emit discoveryFinished(); } + +void PantaboxDiscovery::addResult(Pantabox *connection, const PantaboxUdpDiscovery::DeviceInfo &deviceInfo) +{ + QString modbusSerialNumber = QString::number(connection->serialNumber(), 16).toUpper(); + if (deviceInfo.serialNumber != modbusSerialNumber) { + qCWarning(dcInro()) << "Discovery: Successfully discovered PANTABOX, but the UPD serial number does not match the fetched modbus serial number. Ignoring result..."; + cleanupConnection(connection); + return; + } + + qCDebug(dcInro()) << "Discovery: Connection initialized successfully" << modbusSerialNumber; + + Result result; + result.modbusTcpVersion = Pantabox::modbusVersionToString(connection->modbusTcpVersion()); + result.deviceInfo = deviceInfo; + m_results.append(result); + + qCInfo(dcInro()) << "Discovery: --> Found" + << "Serial number:" << result.deviceInfo.serialNumber + << "(" << connection->serialNumber() << ")" + << "ModbusTCP version:" << result.modbusTcpVersion + << "on" << result.deviceInfo.ipAddress.toString() << result.deviceInfo.macAddress.toString(); + + // Done with this connection + cleanupConnection(connection); +} diff --git a/inro/pantaboxdiscovery.h b/inro/pantaboxdiscovery.h index c0364ce..9210fdd 100644 --- a/inro/pantaboxdiscovery.h +++ b/inro/pantaboxdiscovery.h @@ -31,28 +31,25 @@ #ifndef PANTABOXDISCOVERY_H #define PANTABOXDISCOVERY_H +#include #include -#include - -#include "pantaboxmodbustcpconnection.h" +#include "pantabox.h" +#include "pantaboxudpdiscovery.h" class PantaboxDiscovery : public QObject { Q_OBJECT public: - explicit PantaboxDiscovery(NetworkDeviceDiscovery *networkDeviceDiscovery, QObject *parent = nullptr); + explicit PantaboxDiscovery(QObject *parent = nullptr); typedef struct Result { - QString serialNumber; + PantaboxUdpDiscovery::DeviceInfo deviceInfo; QString modbusTcpVersion; - NetworkDeviceInfo networkDeviceInfo; } Result; QList results() const; - static QString modbusVersionToString(quint32 value); - public slots: void startDiscovery(); @@ -60,20 +57,22 @@ signals: void discoveryFinished(); private: - NetworkDeviceDiscovery *m_networkDeviceDiscovery = nullptr; + PantaboxUdpDiscovery *m_discovery = nullptr; quint16 m_port = 502; quint16 m_modbusAddress = 1; - QDateTime m_startDateTime; + QTimer m_discoveryTimer; - QList m_connections; + QList m_connections; + QList m_alreadyCheckedHosts; QList m_results; - void checkNetworkDevice(const NetworkDeviceInfo &networkDeviceInfo); - void cleanupConnection(PantaboxModbusTcpConnection *connection); + void checkNetworkDevice(const PantaboxUdpDiscovery::DeviceInfo &deviceInfo); + void cleanupConnection(Pantabox *connection); void finishDiscovery(); + void addResult(Pantabox *connection, const PantaboxUdpDiscovery::DeviceInfo &deviceInfo); }; #endif // PANTABOXDISCOVERY_H diff --git a/inro/pantaboxudpdiscovery.cpp b/inro/pantaboxudpdiscovery.cpp new file mode 100644 index 0000000..f1a5973 --- /dev/null +++ b/inro/pantaboxudpdiscovery.cpp @@ -0,0 +1,167 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* Copyright 2013 - 2024, 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 "pantaboxudpdiscovery.h" +#include "extern-plugininfo.h" + +#include +#include +#include +#include "crclookuptable.h" + +#define PANTABOX_DISCOVERY_PORT 52001 + +PantaboxUdpDiscovery::PantaboxUdpDiscovery(QObject *parent) + : QObject{parent} +{ + m_socket = new QUdpSocket(this); + if (!m_socket->bind(QHostAddress::Broadcast, PANTABOX_DISCOVERY_PORT, QAbstractSocket::ShareAddress | QAbstractSocket::ReuseAddressHint)) { + qCWarning(dcInro()) << "UdpDiscovery: Failed to bind to UDP broadcast on" << PANTABOX_DISCOVERY_PORT << m_socket->errorString(); + return; + } + + connect(m_socket, &QUdpSocket::readyRead, this, &PantaboxUdpDiscovery::readPendingDatagrams); + m_available = true; +} + +bool PantaboxUdpDiscovery::available() const +{ + return m_available; +} + +QHash PantaboxUdpDiscovery::results() const +{ + return m_results; +} + +void PantaboxUdpDiscovery::readPendingDatagrams() +{ + while(m_socket->hasPendingDatagrams()) { + + QNetworkDatagram datagram = m_socket->receiveDatagram(); + + for (int i = 0; i < datagram.data().length(); i++) { + + quint8 dataByte = static_cast(datagram.data().at(i)); + + if (!m_prefixStartDiscovered[datagram.senderAddress()] && dataByte == 0xe5) { + m_prefixStartDiscovered[datagram.senderAddress()] = true; + continue; + } + + if (m_prefixStartDiscovered[datagram.senderAddress()] && dataByte == 0x00) { + m_prefixStartDiscovered[datagram.senderAddress()] = false; + + // Paket prefix discovered (0xe5 0x00), process current buffer and start collecting data + processDataBuffer(datagram.senderAddress()); + m_buffers[datagram.senderAddress()].clear(); + continue; + } else { + m_prefixStartDiscovered[datagram.senderAddress()] = false; + } + + // Adding data byte + m_buffers[datagram.senderAddress()].append(dataByte); + + if (m_buffers[datagram.senderAddress()].length() >= 0xffff) { + qCWarning(dcInro()) << "UdpDiscovery: Buffer overflow. Wipe data buffer..."; + m_buffers[datagram.senderAddress()].clear(); + } + } + } +} + +quint8 PantaboxUdpDiscovery::calculateCrc8(const QByteArray &data) +{ + // CRC-8/NRSC-5 initial value + quint8 crc = 0xFF; + + for (quint8 byte : data) { + crc = crc8LookupTable[crc ^ byte]; + } + + return crc; +} + +void PantaboxUdpDiscovery::processDataBuffer(const QHostAddress &address) +{ + if (m_buffers[address].length() < 3) + return; + + quint8 receivedCrc = static_cast(m_buffers[address].at(m_buffers[address].length() - 1)); + quint8 calculatedCrc = calculateCrc8(QByteArray::fromHex("e500").append(m_buffers[address].left(m_buffers[address].length() - 1))); + + if (calculatedCrc != receivedCrc) { + qCDebug(dcInro()) << "UdpDiscovery: Crc checksum not correct. Received crc "<< receivedCrc <<", calculated crc " << calculatedCrc ; + return; + } + + QJsonParseError jsonError; + QJsonDocument jsonDoc = QJsonDocument::fromJson(m_buffers[address].mid(2, m_buffers[address].length() - 3), &jsonError); + + if (jsonError.error != QJsonParseError::NoError) { + qCDebug(dcInro()) << "UdpDiscovery: Received invalud json data" << jsonError.errorString(); + return; + } + + //qCDebug(dcInro()) << "UdpDiscovery:" << qUtf8Printable(jsonDoc.toJson(QJsonDocument::Indented)); + + /* + { + "deviceId": "e45749d4-8c05-44b2-9dbc-xxxxxxxxxxxx", + "encryptBLE": 1, + "env": "live", + "fwVersion": "V1.19.6", + "ipAddress": "10.10.10.111", + "macAddress": "8C:4B:14:88:05:00", + "name": "#1XXXXXXX", + "productKey": "inro-test-1", + "serialNumber": "#1XXXXXXX", + "useTLS": 1 + } + */ + + QVariantMap dataMap = jsonDoc.toVariant().toMap(); + if (dataMap.contains("serialNumber") && dataMap.contains("ipAddress") && dataMap.contains("macAddress")) { + DeviceInfo pantabox; + pantabox.serialNumber = dataMap.value("serialNumber").toString().remove("#"); + pantabox.macAddress = MacAddress(dataMap.value("macAddress").toString()); + pantabox.ipAddress = QHostAddress(dataMap.value("ipAddress").toString()); + + if (address != pantabox.ipAddress) { + qCDebug(dcInro()) << "UdpDiscovery: Received UPD discovery paket from a different IP than communicated in the paket. Ignoring paket."; + return; + } + + qCDebug(dcInro()) << "UdpDiscovery: --> Received discovery paket from" << pantabox.serialNumber << pantabox.macAddress.toString() << pantabox.ipAddress.toString(); + m_results[pantabox.serialNumber] = pantabox; + emit pantaboxDiscovered(pantabox); + } +} diff --git a/inro/pantaboxudpdiscovery.h b/inro/pantaboxudpdiscovery.h new file mode 100644 index 0000000..a7c979f --- /dev/null +++ b/inro/pantaboxudpdiscovery.h @@ -0,0 +1,74 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* Copyright 2013 - 2024, 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 PANTABOXUDPDISCOVERY_H +#define PANTABOXUDPDISCOVERY_H + +#include +#include + +#include + +class PantaboxUdpDiscovery : public QObject +{ + Q_OBJECT +public: + explicit PantaboxUdpDiscovery(QObject *parent = nullptr); + + typedef struct DeviceInfo { + QString serialNumber; + MacAddress macAddress; + QHostAddress ipAddress; + } DeviceInfo; + + bool available() const; + + QHash results() const; + +signals: + void pantaboxDiscovered(const PantaboxUdpDiscovery::DeviceInfo &deviceInfo); + +private slots: + void readPendingDatagrams(); + +private: + QUdpSocket *m_socket = nullptr; + bool m_available = false; + + QHash m_buffers; + QHash m_prefixStartDiscovered; + + quint8 calculateCrc8(const QByteArray &data); + void processDataBuffer(const QHostAddress &address); + + QHash m_results; +}; + +#endif // PANTABOXUDPDISCOVERY_H