feat(v2c): add integration plugin, JSON metadata, and build plumbing
Étape 1 complete: V2C Trydan nymea plugin (Modbus TCP, status Partial).
Plugin (IntegrationPluginV2c):
- discoverThings: delegates to V2cTcpDiscovery, maps results to
ThingDescriptors; createMethod "user" bypasses discovery.
- setupThing: registers a NetworkDeviceMonitor (IP tracking on DHCP
renewal), creates TrydanModbusTcpMaster, wires reachableChanged →
initialize() → state update on initializationFinished(), then starts
periodic polls on a 30s PluginTimer.
- updateThingStates: ChargeState 0/1/2 → pluggedIn/charging;
power = (PauseState==0 AND Lock==0) per evcc trydan.go Enabled().
- handleDynamicConflict: if Dynamic==1 (V2C internal PV optimizer),
write PauseDynamic=1 to suppress it. NEVER write Dynamic=0 (silences
ChargePower telemetry). Writes only on state transition to minimize
flash wear. cf. github.com/evcc-io/evcc/issues/28047.
- Action "power": writes PauseState then Lock (always in mirror), then
fire-and-forgets PauseDynamic if Dynamic==1. Action handlers filter
writeCompleted by register address to avoid reacting to concurrent
background writes.
- Action "maxChargingCurrent": clamps to [minIntensity, maxIntensity],
writes Intensity. Below 6A → pause instead of invalid setpoint.
JSON (integrationpluginv2c.json):
- ThingClass "trydan" implementing evcharger + connectable + networkdevice.
- settingsTypes: suspendInternalOptimizer (bool, default true) — allows
the user to disable Dynamic conflict management if they want the V2C
PID to remain active alongside the HEMS.
- State "chargeEnergy" exposed as diagnostic only (NOT sessionEnergy) —
firmware reliability on this register is unvalidated; cf. evcc #28047.
Build (v2c.pro):
- MODBUS_CONNECTIONS intentionally empty: the modbus-tool code generator
assumes non-overlapping block reads; leaving it empty skips generation
while still linking nymea-modbus via the PKGCONFIG in modbus.pri.
VendorId 56c3e7bb… is new (no existing V2C vendor in the repo).
PluginId f0692725… is new (generated for this plugin).
PORTING_STATUS_modbus.md documents register sources, the 4 items that
require validation on real hardware, and the "no beta matrix" constraint.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
e774f8ee8a
commit
140860aa70
@ -6,7 +6,8 @@ TEMPLATE = subdirs
|
||||
PLUGIN_DIRS = \
|
||||
eastron \
|
||||
abbb2x \
|
||||
abbterra
|
||||
abbterra \
|
||||
v2c
|
||||
|
||||
|
||||
message(============================================)
|
||||
|
||||
330
v2c/integrationpluginv2c.cpp
Normal file
330
v2c/integrationpluginv2c.cpp
Normal file
@ -0,0 +1,330 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#include "integrationpluginv2c.h"
|
||||
#include "v2ctcpdiscovery.h"
|
||||
#include "plugininfo.h"
|
||||
|
||||
#include <integrations/thing.h>
|
||||
#include <integrations/thingactioninfo.h>
|
||||
#include <integrations/thingdescriptor.h>
|
||||
#include <integrations/thingdiscoveryinfo.h>
|
||||
#include <integrations/thingsetupinfo.h>
|
||||
#include <hardwaremanager.h>
|
||||
#include <network/networkdevicediscovery.h>
|
||||
|
||||
IntegrationPluginV2c::IntegrationPluginV2c()
|
||||
{}
|
||||
|
||||
void IntegrationPluginV2c::discoverThings(ThingDiscoveryInfo *info)
|
||||
{
|
||||
V2cTcpDiscovery *discovery = new V2cTcpDiscovery(hardwareManager()->networkDeviceDiscovery(), info);
|
||||
|
||||
connect(discovery, &V2cTcpDiscovery::discoveryFinished, info, [this, info, discovery]() {
|
||||
for (const V2cTcpDiscovery::Result &result : discovery->results()) {
|
||||
ThingDescriptor descriptor(trydanThingClassId,
|
||||
QStringLiteral("V2C Trydan"),
|
||||
result.ipAddress);
|
||||
ParamList params;
|
||||
params.append(Param(trydanThingMacAddressParamTypeId,
|
||||
result.networkDeviceInfo.thingParamValueMacAddress()));
|
||||
params.append(Param(trydanThingHostNameParamTypeId,
|
||||
result.networkDeviceInfo.thingParamValueHostName()));
|
||||
params.append(Param(trydanThingAddressParamTypeId,
|
||||
result.networkDeviceInfo.thingParamValueAddress()));
|
||||
params.append(Param(trydanThingPortParamTypeId, 502));
|
||||
descriptor.setParams(params);
|
||||
|
||||
if (Thing *existing = myThings().findByParams(params)) {
|
||||
descriptor.setThingId(existing->id());
|
||||
}
|
||||
info->addThingDescriptor(descriptor);
|
||||
}
|
||||
info->finish(Thing::ThingErrorNoError);
|
||||
});
|
||||
|
||||
discovery->startDiscovery();
|
||||
}
|
||||
|
||||
void IntegrationPluginV2c::setupThing(ThingSetupInfo *info)
|
||||
{
|
||||
Thing *thing = info->thing();
|
||||
|
||||
if (m_tcpMasters.contains(thing)) {
|
||||
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);
|
||||
}
|
||||
|
||||
const quint16 port = static_cast<quint16>(thing->paramValue(trydanThingPortParamTypeId).toUInt());
|
||||
const quint16 slaveId = 1; // V2C Trydan always uses unit ID 1
|
||||
|
||||
TrydanModbusTcpMaster *master = new TrydanModbusTcpMaster(
|
||||
monitor->networkDeviceInfo().address(), port, slaveId, thing);
|
||||
|
||||
// Update target IP if the device gets a new DHCP lease.
|
||||
connect(monitor, &NetworkDeviceMonitor::networkDeviceInfoChanged,
|
||||
master, [master](const NetworkDeviceInfo &info) {
|
||||
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.
|
||||
connect(master, &TrydanModbusMaster::reachableChanged, thing,
|
||||
[this, thing, master](bool reachable) {
|
||||
if (reachable) {
|
||||
master->initialize();
|
||||
} else {
|
||||
setDisconnectedState(thing);
|
||||
}
|
||||
});
|
||||
|
||||
// After init, set the current-range and report setup done.
|
||||
connect(master, &TrydanModbusMaster::initializationFinished, thing,
|
||||
[this, thing, master](bool success) {
|
||||
if (!success) {
|
||||
return;
|
||||
}
|
||||
thing->setStateValue(trydanConnectedStateTypeId, true);
|
||||
thing->setStateMinMaxValues(trydanMaxChargingCurrentStateTypeId,
|
||||
master->minIntensity(),
|
||||
master->maxIntensity());
|
||||
});
|
||||
|
||||
connect(master, &TrydanModbusMaster::initializationFinished, info,
|
||||
[this, info, thing, master](bool success) {
|
||||
if (!success) {
|
||||
master->deleteLater();
|
||||
info->finish(Thing::ThingErrorHardwareFailure,
|
||||
QT_TR_NOOP("Could not communicate with the V2C Trydan charger."));
|
||||
return;
|
||||
}
|
||||
m_tcpMasters.insert(thing, master);
|
||||
master->update();
|
||||
info->finish(Thing::ThingErrorNoError);
|
||||
});
|
||||
|
||||
// Each completed poll triggers a full state refresh + conflict handling.
|
||||
connect(master, &TrydanModbusMaster::updateFinished, thing,
|
||||
[this, thing, master]() {
|
||||
updateThingStates(thing, master);
|
||||
handleDynamicConflict(thing, master);
|
||||
});
|
||||
|
||||
master->connectDevice();
|
||||
}
|
||||
|
||||
void IntegrationPluginV2c::postSetupThing(Thing *thing)
|
||||
{
|
||||
Q_UNUSED(thing)
|
||||
|
||||
if (m_pluginTimer)
|
||||
return;
|
||||
|
||||
m_pluginTimer = hardwareManager()->pluginTimerManager()->registerTimer(30);
|
||||
connect(m_pluginTimer, &PluginTimer::timeout, this, [this]() {
|
||||
for (TrydanModbusTcpMaster *master : m_tcpMasters) {
|
||||
master->update();
|
||||
}
|
||||
});
|
||||
m_pluginTimer->start();
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
void IntegrationPluginV2c::executeAction(ThingActionInfo *info)
|
||||
{
|
||||
Thing *thing = info->thing();
|
||||
TrydanModbusTcpMaster *master = m_tcpMasters.value(thing);
|
||||
|
||||
if (!master || !master->reachable()) {
|
||||
info->finish(Thing::ThingErrorHardwareNotAvailable,
|
||||
QT_TR_NOOP("The V2C Trydan charger is not reachable."));
|
||||
return;
|
||||
}
|
||||
|
||||
if (info->action().actionTypeId() == trydanPowerActionTypeId) {
|
||||
const bool enable = info->action().paramValue(trydanPowerActionPowerParamTypeId).toBool();
|
||||
|
||||
// setChargingEnabled — always mirror PauseState and Lock together.
|
||||
// cf. evcc trydan.go Enable(): both registers must be written consistently.
|
||||
// PauseState: 1=paused(disabled) / 0=active(enabled)
|
||||
// Lock: 1=locked(disabled) / 0=unlocked(enabled)
|
||||
const quint16 pauseVal = enable ? 0 : 1;
|
||||
const quint16 lockVal = enable ? 0 : 1;
|
||||
|
||||
// writeCompleted carries the originating register address so we can ignore
|
||||
// concurrent background writes (e.g. PauseDynamic from conflict management).
|
||||
master->writePauseState(pauseVal);
|
||||
connect(master, &TrydanModbusMaster::writeCompleted, info,
|
||||
[this, info, thing, master, lockVal, enable]
|
||||
(quint16 addr, bool ok) {
|
||||
if (addr != TrydanModbusMaster::WRegPauseState) return; // ignore others
|
||||
disconnect(master, &TrydanModbusMaster::writeCompleted, info, nullptr);
|
||||
if (!ok) { info->finish(Thing::ThingErrorHardwareFailure); return; }
|
||||
|
||||
master->writeLock(lockVal);
|
||||
connect(master, &TrydanModbusMaster::writeCompleted, info,
|
||||
[this, info, thing, master, enable](quint16 addr2, bool ok2) {
|
||||
if (addr2 != TrydanModbusMaster::WRegLock) return; // ignore others
|
||||
disconnect(master, &TrydanModbusMaster::writeCompleted, info, nullptr);
|
||||
if (ok2) {
|
||||
thing->setStateValue(trydanPowerStateTypeId, enable);
|
||||
// Fire-and-forget: apply PauseDynamic conflict suppression.
|
||||
// Does not block the action — next poll will reflect the result.
|
||||
if (master->dynamicMode() == 1) {
|
||||
const bool suspend = thing->setting(
|
||||
trydanSettingsSuspendInternalOptimizerParamTypeId).toBool();
|
||||
if (suspend)
|
||||
master->writePauseDynamic(enable ? 1 : 0);
|
||||
}
|
||||
master->update();
|
||||
info->finish(Thing::ThingErrorNoError);
|
||||
} else {
|
||||
info->finish(Thing::ThingErrorHardwareFailure);
|
||||
}
|
||||
});
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (info->action().actionTypeId() == trydanMaxChargingCurrentActionTypeId) {
|
||||
double current = info->action().paramValue(
|
||||
trydanMaxChargingCurrentActionMaxChargingCurrentParamTypeId).toDouble();
|
||||
|
||||
// Clamp to IEC 61851 minimum. Below 6 A → pause rather than send invalid setpoint.
|
||||
if (current < 6.0) {
|
||||
master->writePauseState(1);
|
||||
master->writeLock(1);
|
||||
thing->setStateValue(trydanPowerStateTypeId, false);
|
||||
info->finish(Thing::ThingErrorNoError);
|
||||
return;
|
||||
}
|
||||
current = qMin(current, static_cast<double>(master->maxIntensity()));
|
||||
|
||||
const quint16 amps = static_cast<quint16>(qRound(current));
|
||||
master->writeIntensity(amps);
|
||||
connect(master, &TrydanModbusMaster::writeCompleted, info,
|
||||
[this, info, thing, master, current](quint16 addr, bool ok) {
|
||||
if (addr != TrydanModbusMaster::WRegIntensity) return;
|
||||
disconnect(master, &TrydanModbusMaster::writeCompleted, info, nullptr);
|
||||
if (ok) {
|
||||
thing->setStateValue(trydanMaxChargingCurrentStateTypeId, current);
|
||||
master->update();
|
||||
info->finish(Thing::ThingErrorNoError);
|
||||
} else {
|
||||
info->finish(Thing::ThingErrorHardwareFailure);
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
info->finish(Thing::ThingErrorUnsupportedFeature);
|
||||
}
|
||||
|
||||
// --- Private ---
|
||||
|
||||
void IntegrationPluginV2c::updateThingStates(Thing *thing, TrydanModbusMaster *master)
|
||||
{
|
||||
thing->setStateValue(trydanConnectedStateTypeId, master->reachable());
|
||||
|
||||
// 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();
|
||||
thing->setStateValue(trydanPluggedInStateTypeId, cs >= 1);
|
||||
thing->setStateValue(trydanChargingStateTypeId, cs == 2);
|
||||
|
||||
// chargingEnabled = PauseState==0 AND Lock==0 (cf. evcc trydan.go Enabled()).
|
||||
thing->setStateValue(trydanPowerStateTypeId,
|
||||
master->pauseState() == 0 && master->lock() == 0);
|
||||
|
||||
thing->setStateValue(trydanCurrentPowerStateTypeId, static_cast<double>(master->chargePower()));
|
||||
thing->setStateValue(trydanHousePowerStateTypeId, static_cast<double>(master->housePower()));
|
||||
thing->setStateValue(trydanSolarPowerStateTypeId, static_cast<double>(master->powerFV()));
|
||||
thing->setStateValue(trydanSlaveErrorStateTypeId, static_cast<uint>(master->slaveError()));
|
||||
thing->setStateValue(trydanChargeEnergyStateTypeId, static_cast<double>(master->chargeEnergy()));
|
||||
thing->setStateValue(trydanIntensityStateTypeId, static_cast<double>(master->intensity()));
|
||||
|
||||
// Dynamic==1 means the V2C internal PV optimizer is configured and running.
|
||||
const bool conflict = (master->dynamicMode() == 1);
|
||||
thing->setStateValue(trydanConflictDetectedStateTypeId, conflict);
|
||||
thing->setStateValue(trydanOptimizerSuspendedStateTypeId, master->pauseDynamic() == 1);
|
||||
|
||||
if (conflict) {
|
||||
qCDebug(dcV2c()) << thing->name()
|
||||
<< ": V2C internal optimizer is active (Dynamic==1). "
|
||||
"HEMS will suppress it via PauseDynamic if suspendInternalOptimizer==true.";
|
||||
}
|
||||
}
|
||||
|
||||
void IntegrationPluginV2c::handleDynamicConflict(Thing *thing, TrydanModbusMaster *master)
|
||||
{
|
||||
// The V2C Trydan embeds its own solar-tracking PID ("Dynamic mode").
|
||||
// Two controllers must not drive the same charger simultaneously.
|
||||
//
|
||||
// Correct suppression: write PauseDynamic=1 to freeze the internal PID.
|
||||
// WRONG approach: write Dynamic=0 — this silences ChargePower telemetry.
|
||||
// cf. evcc charger/trydan.go and github.com/evcc-io/evcc/issues/28047.
|
||||
//
|
||||
// PauseDynamic is only relevant when Dynamic==1 (optimizer configured on device).
|
||||
// We re-evaluate every poll because the V2C app can toggle Dynamic at any time.
|
||||
|
||||
if (master->dynamicMode() != 1)
|
||||
return; // No internal optimizer — nothing to manage.
|
||||
|
||||
const bool suspendAllowed = thing->setting(trydanSettingsSuspendInternalOptimizerParamTypeId).toBool();
|
||||
if (!suspendAllowed) {
|
||||
qCWarning(dcV2c()) << thing->name()
|
||||
<< ": V2C internal optimizer active but suspendInternalOptimizer==false. "
|
||||
"HEMS and V2C PID may fight — expect oscillations.";
|
||||
return;
|
||||
}
|
||||
|
||||
// HEMS is in control when chargingEnabled is true.
|
||||
const bool hemsInControl = (master->pauseState() == 0 && master->lock() == 0);
|
||||
const int desired = hemsInControl ? 1 : 0;
|
||||
|
||||
// Only write on transition to avoid unnecessary flash wear.
|
||||
if (master->pauseDynamic() != desired) {
|
||||
qCDebug(dcV2c()) << thing->name()
|
||||
<< ": writing PauseDynamic =" << desired
|
||||
<< (hemsInControl ? "(HEMS taking control)" : "(releasing to V2C PID)");
|
||||
master->writePauseDynamic(static_cast<quint16>(desired));
|
||||
}
|
||||
}
|
||||
|
||||
void IntegrationPluginV2c::setDisconnectedState(Thing *thing)
|
||||
{
|
||||
thing->setStateValue(trydanConnectedStateTypeId, false);
|
||||
thing->setStateValue(trydanChargingStateTypeId, false);
|
||||
thing->setStateValue(trydanPluggedInStateTypeId, false);
|
||||
thing->setStateValue(trydanCurrentPowerStateTypeId, 0.0);
|
||||
}
|
||||
68
v2c/integrationpluginv2c.h
Normal file
68
v2c/integrationpluginv2c.h
Normal file
@ -0,0 +1,68 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#ifndef INTEGRATIONPLUGINV2C_H
|
||||
#define INTEGRATIONPLUGINV2C_H
|
||||
|
||||
#include <plugintimer.h>
|
||||
#include <integrations/integrationplugin.h>
|
||||
#include <network/networkdevicemonitor.h>
|
||||
|
||||
#include "extern-plugininfo.h"
|
||||
#include "trydanmodbusmaster.h"
|
||||
#include "trydanmodbustcpmaster.h"
|
||||
|
||||
/*!
|
||||
* \brief nymea integration plugin for the V2C Trydan EV charger.
|
||||
*
|
||||
* ThingClass \c trydan implementing the \c evcharger interface via Modbus TCP
|
||||
* (Étape 1). All register-level I/O is delegated to TrydanModbusTcpMaster;
|
||||
* this class owns the evcharger business logic:
|
||||
* - ChargeState → pluggedIn / charging mapping
|
||||
* - chargingEnabled = (PauseState==0 AND Lock==0)
|
||||
* - Dynamic optimizer conflict management via PauseDynamic
|
||||
*
|
||||
* Étape 2 (RTU) will add TrydanModbusRtuMaster on the same TrydanModbusMaster
|
||||
* interface; the logic in this file requires no changes.
|
||||
*/
|
||||
class IntegrationPluginV2c : public IntegrationPlugin
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
Q_PLUGIN_METADATA(IID "io.nymea.IntegrationPlugin" FILE "integrationpluginv2c.json")
|
||||
Q_INTERFACES(IntegrationPlugin)
|
||||
|
||||
public:
|
||||
explicit IntegrationPluginV2c();
|
||||
|
||||
void discoverThings(ThingDiscoveryInfo *info) override;
|
||||
void setupThing(ThingSetupInfo *info) override;
|
||||
void postSetupThing(Thing *thing) override;
|
||||
void thingRemoved(Thing *thing) override;
|
||||
|
||||
public slots:
|
||||
void executeAction(ThingActionInfo *info) override;
|
||||
|
||||
private:
|
||||
/*! \brief Apply all evcharger states from the last completed poll. */
|
||||
void updateThingStates(Thing *thing, TrydanModbusMaster *master);
|
||||
|
||||
/*!
|
||||
* \brief Manage the Dynamic/PauseDynamic conflict on each poll.
|
||||
*
|
||||
* If Dynamic==1 (V2C internal optimizer is running) and HEMS is in control
|
||||
* (chargingEnabled==true), write PauseDynamic=1 to suppress the internal PID
|
||||
* without disabling the telemetry. Release PauseDynamic=0 when HEMS
|
||||
* relinquishes control. Dynamic is NEVER written (would silence ChargePower).
|
||||
* cf. AGENTS.md § CONFLIT and evcc charger/trydan.go.
|
||||
*/
|
||||
void handleDynamicConflict(Thing *thing, TrydanModbusMaster *master);
|
||||
|
||||
void setDisconnectedState(Thing *thing);
|
||||
|
||||
PluginTimer *m_pluginTimer = nullptr;
|
||||
|
||||
QHash<Thing *, TrydanModbusTcpMaster *> m_tcpMasters;
|
||||
QHash<Thing *, NetworkDeviceMonitor *> m_monitors;
|
||||
};
|
||||
|
||||
#endif // INTEGRATIONPLUGINV2C_H
|
||||
187
v2c/integrationpluginv2c.json
Normal file
187
v2c/integrationpluginv2c.json
Normal file
@ -0,0 +1,187 @@
|
||||
{
|
||||
"name": "V2C",
|
||||
"displayName": "V2C Trydan",
|
||||
"id": "f0692725-650a-47c3-a673-172f077768f1",
|
||||
"paramTypes": [],
|
||||
"vendors": [
|
||||
{
|
||||
"id": "56c3e7bb-2db8-4002-b799-9e21530e6fb9",
|
||||
"name": "v2c",
|
||||
"displayName": "V2C",
|
||||
"thingClasses": [
|
||||
{
|
||||
"id": "b2a12873-4d11-444e-a4ad-914907491eb4",
|
||||
"name": "trydan",
|
||||
"displayName": "Trydan",
|
||||
"interfaces": [
|
||||
"evcharger",
|
||||
"connectable",
|
||||
"networkdevice"
|
||||
],
|
||||
"createMethods": [
|
||||
"discovery",
|
||||
"user"
|
||||
],
|
||||
"discoveryType": "weak",
|
||||
"paramTypes": [
|
||||
{
|
||||
"id": "2fe17a54-b24b-4868-ae99-36a4627e6827",
|
||||
"name": "macAddress",
|
||||
"displayName": "MAC address",
|
||||
"type": "QString",
|
||||
"inputType": "MacAddress",
|
||||
"defaultValue": "",
|
||||
"readOnly": true
|
||||
},
|
||||
{
|
||||
"id": "c0cdfa64-7054-4486-809e-3b7f257a3aab",
|
||||
"name": "address",
|
||||
"displayName": "Host address",
|
||||
"type": "QString",
|
||||
"inputType": "IPv4Address",
|
||||
"defaultValue": ""
|
||||
},
|
||||
{
|
||||
"id": "bd9987a4-4c45-49c8-ab67-8c80705ec109",
|
||||
"name": "hostName",
|
||||
"displayName": "Host name",
|
||||
"type": "QString",
|
||||
"inputType": "TextLine",
|
||||
"defaultValue": ""
|
||||
},
|
||||
{
|
||||
"id": "d096bfe1-5861-456f-915c-f1f2c65b03b3",
|
||||
"name": "port",
|
||||
"displayName": "Modbus TCP port",
|
||||
"type": "uint",
|
||||
"defaultValue": 502
|
||||
}
|
||||
],
|
||||
"settingsTypes": [
|
||||
{
|
||||
"id": "a47f20bd-dfdc-4827-ab7d-05d7819f316e",
|
||||
"name": "suspendInternalOptimizer",
|
||||
"displayName": "Suspend internal V2C optimizer when HEMS takes control",
|
||||
"type": "bool",
|
||||
"defaultValue": true
|
||||
}
|
||||
],
|
||||
"stateTypes": [
|
||||
{
|
||||
"id": "47054399-82fb-4e12-9bb6-9ae3e4fd4f78",
|
||||
"name": "connected",
|
||||
"displayName": "Connected",
|
||||
"type": "bool",
|
||||
"defaultValue": false,
|
||||
"cached": false
|
||||
},
|
||||
{
|
||||
"id": "f38c313c-47cc-4791-8a8f-64c1af852af6",
|
||||
"name": "pluggedIn",
|
||||
"displayName": "Plugged in",
|
||||
"type": "bool",
|
||||
"defaultValue": false,
|
||||
"cached": false
|
||||
},
|
||||
{
|
||||
"id": "6d003e09-d30d-4a1c-9e2d-9e838b61dc5d",
|
||||
"name": "charging",
|
||||
"displayName": "Charging",
|
||||
"type": "bool",
|
||||
"defaultValue": false,
|
||||
"cached": false
|
||||
},
|
||||
{
|
||||
"id": "eb3253a2-7518-4e76-9c06-1bb133d5037d",
|
||||
"name": "power",
|
||||
"displayName": "Charging enabled",
|
||||
"displayNameAction": "Set charging enabled",
|
||||
"type": "bool",
|
||||
"defaultValue": true,
|
||||
"writable": true
|
||||
},
|
||||
{
|
||||
"id": "deb8826d-4f91-42b8-89cf-16feb6425e0e",
|
||||
"name": "maxChargingCurrent",
|
||||
"displayName": "Maximum charging current",
|
||||
"displayNameAction": "Set maximum charging current",
|
||||
"type": "double",
|
||||
"unit": "Ampere",
|
||||
"minValue": 6,
|
||||
"maxValue": 32,
|
||||
"stepSize": 1,
|
||||
"defaultValue": 16,
|
||||
"writable": true
|
||||
},
|
||||
{
|
||||
"id": "2570c0d7-f9a5-4e24-9242-6d79253004ec",
|
||||
"name": "currentPower",
|
||||
"displayName": "Charging power",
|
||||
"type": "double",
|
||||
"unit": "Watt",
|
||||
"defaultValue": 0,
|
||||
"cached": false
|
||||
},
|
||||
{
|
||||
"id": "d11a392f-387d-4f10-9050-e25198d2c584",
|
||||
"name": "housePower",
|
||||
"displayName": "House power (CT clamp)",
|
||||
"type": "double",
|
||||
"unit": "Watt",
|
||||
"defaultValue": 0,
|
||||
"cached": false
|
||||
},
|
||||
{
|
||||
"id": "37e706fb-fa3c-484a-b776-b50aa5de5c61",
|
||||
"name": "solarPower",
|
||||
"displayName": "Solar power (PV seen by charger)",
|
||||
"type": "double",
|
||||
"unit": "Watt",
|
||||
"defaultValue": 0,
|
||||
"cached": false
|
||||
},
|
||||
{
|
||||
"id": "9a9e6afb-a641-409f-9705-c16776db3a29",
|
||||
"name": "slaveError",
|
||||
"displayName": "Charger error code",
|
||||
"type": "uint",
|
||||
"defaultValue": 0
|
||||
},
|
||||
{
|
||||
"id": "fe616e45-39cb-4be3-8426-9d27c7a64f06",
|
||||
"name": "chargeEnergy",
|
||||
"displayName": "Session energy (diagnostic)",
|
||||
"type": "double",
|
||||
"unit": "KiloWattHour",
|
||||
"defaultValue": 0
|
||||
},
|
||||
{
|
||||
"id": "71773194-ebd3-4ded-893c-14bfef9b1854",
|
||||
"name": "intensity",
|
||||
"displayName": "Current charging current",
|
||||
"type": "double",
|
||||
"unit": "Ampere",
|
||||
"defaultValue": 0,
|
||||
"cached": false
|
||||
},
|
||||
{
|
||||
"id": "3c86495b-74da-440e-98a5-825678e35c1a",
|
||||
"name": "conflictDetected",
|
||||
"displayName": "Internal V2C optimizer active",
|
||||
"type": "bool",
|
||||
"defaultValue": false
|
||||
},
|
||||
{
|
||||
"id": "006dd018-9c6a-4a4e-a7b2-eb965d27ca92",
|
||||
"name": "optimizerSuspended",
|
||||
"displayName": "Internal optimizer suspended by HEMS",
|
||||
"type": "bool",
|
||||
"defaultValue": false
|
||||
}
|
||||
],
|
||||
"actionTypes": []
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
12
v2c/meta.json
Normal file
12
v2c/meta.json
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"title": "V2C Trydan",
|
||||
"tagline": "Connect V2C Trydan EV chargers over Modbus TCP.",
|
||||
"stability": "consumer",
|
||||
"offline": true,
|
||||
"technologies": [
|
||||
"network"
|
||||
],
|
||||
"categories": [
|
||||
"energy"
|
||||
]
|
||||
}
|
||||
@ -0,0 +1,19 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!DOCTYPE TS>
|
||||
<TS version="2.1" language="en_US">
|
||||
<context>
|
||||
<name>IntegrationPluginV2c</name>
|
||||
<message>
|
||||
<source>Could not register network monitor for this charger.</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Could not communicate with the V2C Trydan charger.</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>The V2C Trydan charger is not reachable.</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
</context>
|
||||
</TS>
|
||||
23
v2c/v2c.pro
Normal file
23
v2c/v2c.pro
Normal file
@ -0,0 +1,23 @@
|
||||
include(../plugins.pri)
|
||||
|
||||
# nymea-modbus provides ModbusTcpMaster and float-decode utils.
|
||||
# MODBUS_CONNECTIONS is intentionally empty: V2C Trydan register windows
|
||||
# overlap (see AGENTS.md), so the auto-generator's block-read optimisation
|
||||
# would corrupt the readings. All I/O is hand-coded in TrydanModbusTcpMaster.
|
||||
include(../modbus.pri)
|
||||
|
||||
QT += network
|
||||
|
||||
TARGET = $$qtLibraryTarget(nymea_integrationpluginv2c)
|
||||
|
||||
SOURCES += \
|
||||
integrationpluginv2c.cpp \
|
||||
trydanmodbusmaster.cpp \
|
||||
trydanmodbustcpmaster.cpp \
|
||||
v2ctcpdiscovery.cpp
|
||||
|
||||
HEADERS += \
|
||||
integrationpluginv2c.h \
|
||||
trydanmodbusmaster.h \
|
||||
trydanmodbustcpmaster.h \
|
||||
v2ctcpdiscovery.h
|
||||
Loading…
x
Reference in New Issue
Block a user