diff --git a/v2c/integrationpluginv2c.cpp b/v2c/integrationpluginv2c.cpp index fba02de..d169195 100644 --- a/v2c/integrationpluginv2c.cpp +++ b/v2c/integrationpluginv2c.cpp @@ -12,6 +12,8 @@ #include #include +#include + IntegrationPluginV2c::IntegrationPluginV2c() {} @@ -53,65 +55,37 @@ void IntegrationPluginV2c::setupThing(ThingSetupInfo *info) m_tcpMasters.take(thing)->deleteLater(); } - // Register or reuse the network monitor (tracks IP changes when DHCP renews). - NetworkDeviceMonitor *monitor = m_monitors.value(thing); - if (!monitor) { - monitor = hardwareManager()->networkDeviceDiscovery()->registerMonitor(thing); - if (!monitor) { - info->finish(Thing::ThingErrorInvalidParameter, - QT_TR_NOOP("Could not register network monitor for this charger.")); - return; - } - m_monitors.insert(thing, monitor); - } - + // Pre-req: the charger must have a static (DHCP-reserved) IP. + // NetworkDeviceMonitor is intentionally absent: its periodic ICMP pings + // saturate the Trydan ESP32 WiFi stack (observed latency > 14 s, causing + // Modbus timeouts). Connectivity is derived from Modbus poll results only. + const QHostAddress address(thing->paramValue(trydanThingAddressParamTypeId).toString()); const quint16 port = static_cast(thing->paramValue(trydanThingPortParamTypeId).toUInt()); const quint16 slaveId = 1; // V2C Trydan always uses unit ID 1 - // Prefer the monitor's cached address (ARP/mDNS already resolved) but fall back - // to the stored address param. The monitor has a null address at first - // registerMonitor() when the device was added by IP (createMethod "user") or when - // nymead restarts before the monitor's ARP scan completes — connecting to a null - // QHostAddress would produce "device ':502'" and fail immediately. - QHostAddress address = monitor->networkDeviceInfo().address(); - if (address.isNull()) - address = QHostAddress(thing->paramValue(trydanThingAddressParamTypeId).toString()); - TrydanModbusTcpMaster *master = new TrydanModbusTcpMaster(address, port, slaveId, thing); - // Update target IP when DHCP renews. Guard against transient null info that - // the monitor can emit during its own internal refresh cycles. - connect(monitor, &NetworkDeviceMonitor::networkDeviceInfoChanged, - master, [master](const NetworkDeviceInfo &info) { - if (!info.address().isNull()) - master->setHostAddress(info.address()); - }); - - // Abort the setup if the info object is destroyed before we finish. connect(info, &ThingSetupInfo::aborted, master, &TrydanModbusTcpMaster::deleteLater); - connect(info, &ThingSetupInfo::aborted, monitor, [this, thing]() { - if (m_monitors.contains(thing)) { - hardwareManager()->networkDeviceDiscovery()->unregisterMonitor(m_monitors.take(thing)); - } - }); - // Reachable → initialize; lost → disconnected state. + // Reachable → initialize (reads MinIntensity, MaxIntensity). + // Lost → set disconnected state with timestamp for SAV visibility. connect(master, &TrydanModbusMaster::reachableChanged, thing, [this, thing, master](bool reachable) { if (reachable) { master->initialize(); } else { - setDisconnectedState(thing); + setDisconnectedState(thing, QStringLiteral("timeout Modbus — vérifier signal WiFi de la borne")); } }); - // After init, set the current-range and report setup done. + // After init, expose the configured current range and finish setup. connect(master, &TrydanModbusMaster::initializationFinished, thing, [this, thing, master](bool success) { if (!success) { return; } thing->setStateValue(trydanConnectedStateTypeId, true); + thing->setStateValue(trydanStatusMessageStateTypeId, QString()); thing->setStateMinMaxValues(trydanMaxChargingCurrentStateTypeId, master->minIntensity(), master->maxIntensity()); @@ -150,7 +124,14 @@ void IntegrationPluginV2c::postSetupThing(Thing *thing) m_pluginTimer = hardwareManager()->pluginTimerManager()->registerTimer(30); connect(m_pluginTimer, &PluginTimer::timeout, this, [this]() { for (TrydanModbusTcpMaster *master : m_tcpMasters) { - master->update(); + if (master->reachable()) { + master->update(); + } else { + // TCP connection may have dropped (charger WiFi glitch). + // connectDevice() is idempotent on an already-connecting socket; + // the 30s timer provides the reconnect backoff — no busy-loop. + master->connectDevice(); + } } }); m_pluginTimer->start(); @@ -160,10 +141,6 @@ void IntegrationPluginV2c::thingRemoved(Thing *thing) { delete m_tcpMasters.take(thing); - if (m_monitors.contains(thing)) { - hardwareManager()->networkDeviceDiscovery()->unregisterMonitor(m_monitors.take(thing)); - } - if (myThings().isEmpty() && m_pluginTimer) { hardwareManager()->pluginTimerManager()->unregisterTimer(m_pluginTimer); m_pluginTimer = nullptr; @@ -266,6 +243,9 @@ void IntegrationPluginV2c::updateThingStates(Thing *thing, TrydanModbusMaster *m { thing->setStateValue(trydanConnectedStateTypeId, master->reachable()); + // Poll succeeded → clear any previous offline message. + thing->setStateValue(trydanStatusMessageStateTypeId, QString()); + // ChargeState: 0=A(disconnected), 1=B(connected, not charging), 2=C(charging). // cf. IEC 61851 and evcc trydan.go Status(). const int cs = master->chargeState(); @@ -331,10 +311,18 @@ void IntegrationPluginV2c::handleDynamicConflict(Thing *thing, TrydanModbusMaste } } -void IntegrationPluginV2c::setDisconnectedState(Thing *thing) +void IntegrationPluginV2c::setDisconnectedState(Thing *thing, const QString &cause) { thing->setStateValue(trydanConnectedStateTypeId, false); thing->setStateValue(trydanChargingStateTypeId, false); thing->setStateValue(trydanPluggedInStateTypeId, false); thing->setStateValue(trydanCurrentPowerStateTypeId, 0.0); + + // Inform the user with a timestamp so they know when the outage started, + // and a hint pointing to the most likely cause (WiFi signal on this charger). + const QString ts = QDateTime::currentDateTime().toString(QStringLiteral("dd/MM HH:mm")); + const QString msg = cause.isEmpty() + ? QStringLiteral("Hors ligne depuis ") + ts + : QStringLiteral("Hors ligne depuis ") + ts + QStringLiteral(" — ") + cause; + thing->setStateValue(trydanStatusMessageStateTypeId, msg); } diff --git a/v2c/integrationpluginv2c.h b/v2c/integrationpluginv2c.h index 5ae19f9..7d79425 100644 --- a/v2c/integrationpluginv2c.h +++ b/v2c/integrationpluginv2c.h @@ -5,7 +5,6 @@ #include #include -#include #include "extern-plugininfo.h" #include "trydanmodbusmaster.h" @@ -21,6 +20,10 @@ * - chargingEnabled = (PauseState==0 AND Lock==0) * - Dynamic optimizer conflict management via PauseDynamic * + * No NetworkDeviceMonitor: the Trydan ESP32 WiFi stack saturates under + * periodic ICMP pings (latency > 14 s, Modbus timeouts). Connectivity is + * derived from Modbus poll success/failure instead. + * * Étape 2 (RTU) will add TrydanModbusRtuMaster on the same TrydanModbusMaster * interface; the logic in this file requires no changes. */ @@ -57,12 +60,11 @@ private: */ void handleDynamicConflict(Thing *thing, TrydanModbusMaster *master); - void setDisconnectedState(Thing *thing); + void setDisconnectedState(Thing *thing, const QString &cause = QString()); PluginTimer *m_pluginTimer = nullptr; QHash m_tcpMasters; - QHash m_monitors; }; #endif // INTEGRATIONPLUGINV2C_H diff --git a/v2c/integrationpluginv2c.json b/v2c/integrationpluginv2c.json index 31b510e..3901b2d 100644 --- a/v2c/integrationpluginv2c.json +++ b/v2c/integrationpluginv2c.json @@ -15,8 +15,7 @@ "displayName": "Trydan", "interfaces": [ "evcharger", - "connectable", - "networkdevice" + "connectable" ], "createMethods": [ "discovery", @@ -177,6 +176,14 @@ "displayName": "Internal optimizer suspended by HEMS", "type": "bool", "defaultValue": false + }, + { + "id": "c2d4e6f8-1a3b-4c7d-be0f-2a4b6c8d0e1f", + "name": "statusMessage", + "displayName": "Connection status", + "type": "QString", + "defaultValue": "", + "cached": false } ], "actionTypes": [] diff --git a/v2c/trydanmodbustcpmaster.cpp b/v2c/trydanmodbustcpmaster.cpp index 7636391..0c060af 100644 --- a/v2c/trydanmodbustcpmaster.cpp +++ b/v2c/trydanmodbustcpmaster.cpp @@ -16,8 +16,12 @@ TrydanModbusTcpMaster::TrydanModbusTcpMaster(const QHostAddress &address, m_slaveId(slaveId) { m_modbusTcpMaster = new ModbusTcpMaster(address, port, this); - m_modbusTcpMaster->setNumberOfRetries(1); - m_modbusTcpMaster->setTimeout(2000); + // No retry per individual read: abort fast on first timeout so the 30s poll + // cycle is not blocked for minutes on a dropped connection. + m_modbusTcpMaster->setNumberOfRetries(0); + // 5 s tolerates the WiFi latency spikes observed on the Trydan ESP32 + // without waiting forever when the charger is genuinely unreachable. + m_modbusTcpMaster->setTimeout(5000); connect(m_modbusTcpMaster, &ModbusTcpMaster::connectionStateChanged, this, &TrydanModbusTcpMaster::onConnectionStateChanged); diff --git a/v2c/trydanmodbustcpmaster.h b/v2c/trydanmodbustcpmaster.h index 9703799..ec735c6 100644 --- a/v2c/trydanmodbustcpmaster.h +++ b/v2c/trydanmodbustcpmaster.h @@ -101,7 +101,9 @@ private: bool m_updating = false; bool m_initializing = false; - static constexpr int k_errorLimit = 5; + // 3 consecutive failures → mark unreachable. With a 5 s timeout and no + // retry, 3 failed polls abort within 15 s — acceptable for HEMS decisions. + static constexpr int k_errorLimit = 3; }; #endif // TRYDANMODBUSTCPMASTER_H