mirror of
https://github.com/nymea/nymea-plugins.git
synced 2026-10-15 13:23:27 +02:00
EVerest: Add RFID thing class and functionality for the nymea-rfid-provider
This commit is contained in:
parent
4820e3a56f
commit
073ff88f32
@ -6,6 +6,60 @@ This nymea integration plugin allows to connect to any EVerest instance in the n
|
||||
|
||||
By default the integration requires the use of the RpcApi module, which provides a JSON RPC interface based on a websocket communication to the EVerest core.
|
||||
|
||||
## Local AC charger with RFID
|
||||
|
||||
Install and enable `nymea-rfid-provider.service` on the charger before nymead
|
||||
starts. The provider owns the RFID hardware; EVerest must not run its own reader
|
||||
module against it. The first provider backend supports PN7160 through
|
||||
EVerest's `libnfc-nci`, built with libgpiod 2.x support for named GPIO lines.
|
||||
|
||||
When creating a missing RPC charger child, the integration selects **EVerest AC
|
||||
Charger with RFID** if the provider's compatible system D-Bus interface exists
|
||||
and the connection is localhost, has exactly one EVSE with one connector, and
|
||||
supports AC without DC. Provider readiness does not select the class: an
|
||||
unavailable reader still belongs to an RFID-capable charger. Remote connections,
|
||||
DC, MQTT and ambiguous connector topologies retain their existing behavior.
|
||||
|
||||
The provider probe completes before new charger descriptors are emitted.
|
||||
`connected` reports RPC connectivity; `rfidAvailable` reports usable RFID
|
||||
connectivity. Provider restarts restore subscriptions without recreating things.
|
||||
Scans during outages are not replayed. More than one available local connection
|
||||
disables RFID routing rather than assigning a scan to an arbitrary charger.
|
||||
|
||||
Existing charger things are preserved. To change an existing plain AC charger,
|
||||
explicitly remove it and rediscover it by reconnecting/restarting its EVerest
|
||||
connection after enabling the provider. The new thing has a new identity:
|
||||
review thing-linked rules, energy settings and other references; historical data
|
||||
remains associated with the old identity. There is no automatic migration.
|
||||
|
||||
The standard nymea RFID experience handles enrollment, authorization identity and
|
||||
charging policy. The integration supplies its required identity states and
|
||||
enrollment action. It forwards `tagDetected(code)` once per PN7160 presentation;
|
||||
holding a tag does not repeatedly trigger authorization or stop charging.
|
||||
Identifiers use uppercase UID hexadecimal in reader-library byte order with
|
||||
leading zeroes and no separators. Arrival/removal notifications are available
|
||||
only on the provider's D-Bus interface in this version.
|
||||
|
||||
`tagAccepted` and `tagRejected` currently log readable feedback, clear enrollment
|
||||
mode and succeed. They do not call EVerest: the corresponding RPC feedback
|
||||
methods are not yet available. This does not replace the RFID experience's
|
||||
authorization decision.
|
||||
|
||||
### RFID regression tests
|
||||
|
||||
The standalone Qt 6 tests need Qt Core, DBus, Network, WebSockets, Test,
|
||||
`dbus-run-session`, and the sibling nymea source checkout for headers.
|
||||
|
||||
```sh
|
||||
cmake -S everest/tests -B build-everest-rfid-tests -DCMAKE_PREFIX_PATH=/path/to/Qt
|
||||
cmake --build build-everest-rfid-tests -j
|
||||
ctest --test-dir build-everest-rfid-tests --output-on-failure
|
||||
```
|
||||
|
||||
The tests use a private bus and fake provider; real PN7160 tag formats, physical
|
||||
removal, charging enrollment/authorization and system boot ordering require
|
||||
target acceptance testing.
|
||||
|
||||
|
||||
## Using the MQTT based API module
|
||||
|
||||
|
||||
@ -1,9 +1,10 @@
|
||||
include(../plugins.pri)
|
||||
|
||||
QT += network websockets
|
||||
QT += network websockets dbus
|
||||
PKGCONFIG += nymea-mqtt
|
||||
|
||||
SOURCES += \
|
||||
rfid/rfidproviderclient.cpp \
|
||||
jsonrpc/everestconnection.cpp \
|
||||
jsonrpc/everestevse.cpp \
|
||||
jsonrpc/everestjsonrpcclient.cpp \
|
||||
@ -16,6 +17,8 @@ SOURCES += \
|
||||
integrationplugineverest.cpp
|
||||
|
||||
HEADERS += \
|
||||
rfid/rfidproviderclient.h \
|
||||
rfid/everestrfidutils.h \
|
||||
jsonrpc/everestconnection.h \
|
||||
jsonrpc/everestevse.h \
|
||||
jsonrpc/everestjsonrpcclient.h \
|
||||
|
||||
@ -28,9 +28,12 @@
|
||||
#include "mqtt/everestmqttdiscovery.h"
|
||||
#include "jsonrpc/everestevse.h"
|
||||
#include "jsonrpc/everestjsonrpcdiscovery.h"
|
||||
#include "rfid/everestrfidutils.h"
|
||||
|
||||
#include <QFileInfo>
|
||||
#include <QPointer>
|
||||
|
||||
#include <hardwaremanager.h>
|
||||
#include <nymeasettings.h>
|
||||
#include <network/networkdevicediscovery.h>
|
||||
|
||||
@ -41,6 +44,16 @@ IntegrationPluginEverest::IntegrationPluginEverest()
|
||||
|
||||
void IntegrationPluginEverest::init()
|
||||
{
|
||||
m_rfidProvider = new RfidProviderClient(this);
|
||||
connect(m_rfidProvider, &RfidProviderClient::availabilityChanged, this, &IntegrationPluginEverest::updateRfidAvailability);
|
||||
connect(m_rfidProvider, &RfidProviderClient::tagDetected, this, [this](const QString &code, const QVariantMap &) {
|
||||
Thing *target = rfidTarget();
|
||||
if (!target)
|
||||
return;
|
||||
emit emitEvent(Event(everestChargerAcRfidTagDetectedEventTypeId, target->id(),
|
||||
ParamList() << Param(everestChargerAcRfidTagDetectedEventCodeParamTypeId, code)));
|
||||
});
|
||||
m_rfidProvider->probe();
|
||||
QFileInfo enableMqttFileInfo(NymeaSettings::settingsPath() + "/everest-mqtt");
|
||||
m_useMqtt = enableMqttFileInfo.exists();
|
||||
if (m_useMqtt) {
|
||||
@ -399,74 +412,98 @@ void IntegrationPluginEverest::setupThing(ThingSetupInfo *info)
|
||||
m_everstConnections.insert(thing, connection);
|
||||
|
||||
connect(connection, &EverestConnection::availableChanged, thing, [this, thing, connection](bool available){
|
||||
const quint64 generation = connection->property("rfidDiscoveryGeneration").toULongLong() + 1;
|
||||
connection->setProperty("rfidDiscoveryGeneration", generation);
|
||||
thing->setStateValue(everestConnectionConnectedStateTypeId, available);
|
||||
// Update connected state of child things
|
||||
foreach (Thing *child, myThings().filterByParentId(thing->id())) {
|
||||
child->setStateValue("connected", available);
|
||||
}
|
||||
updateRfidAvailability();
|
||||
|
||||
if (available) {
|
||||
thing->setStateValue(everestConnectionApiVersionStateTypeId, connection->client()->apiVersion());
|
||||
|
||||
// Sync things with availabe EvseInfos from this connection
|
||||
// Finish the bounded provider probe before choosing a new child's class.
|
||||
QPointer<Thing> guardedThing(thing);
|
||||
QPointer<EverestConnection> guardedConnection(connection);
|
||||
m_rfidProvider->probe([this, guardedThing, guardedConnection, generation](bool providerAvailable) {
|
||||
if (!guardedThing || !guardedConnection || !guardedConnection->available()
|
||||
|| guardedConnection->property("rfidDiscoveryGeneration").toULongLong() != generation
|
||||
|| m_everstConnections.value(guardedThing) != guardedConnection)
|
||||
return;
|
||||
Thing *thing = guardedThing;
|
||||
EverestConnection *connection = guardedConnection;
|
||||
int localConnections = 0;
|
||||
for (EverestConnection *candidate : m_everstConnections) {
|
||||
const QString host = candidate->client()->serverUrl().host();
|
||||
if (candidate->available() && (QHostAddress(host).isLoopback() || host.compare("localhost", Qt::CaseInsensitive) == 0))
|
||||
++localConnections;
|
||||
}
|
||||
const bool withRfid = providerAvailable && localConnections == 1
|
||||
&& everestRfidEligible(connection->client()->serverUrl(), connection->client()->evseInfos());
|
||||
|
||||
QList<int> indexList;
|
||||
ThingDescriptors descriptors;
|
||||
qCDebug(dcEverest()) << "The client is now available, synching things...";
|
||||
// Sync things with availabe EvseInfos from this connection
|
||||
|
||||
// First check if we have a thing for each index, if not, create the thing for it
|
||||
foreach (const EverestJsonRpcClient::EVSEInfo &evseInfo, connection->client()->evseInfos()) {
|
||||
QList<int> indexList;
|
||||
ThingDescriptors descriptors;
|
||||
qCDebug(dcEverest()) << "The client is now available, synching things...";
|
||||
|
||||
// Collect all EVSE index in onder to check later if we have a thing for a dissapeard index.
|
||||
indexList.append(evseInfo.index);
|
||||
// First check if we have a thing for each index, if not, create the thing for it
|
||||
foreach (const EverestJsonRpcClient::EVSEInfo &evseInfo, connection->client()->evseInfos()) {
|
||||
|
||||
// Check if we already have a child device for this index
|
||||
bool alreadyAdded = false;
|
||||
// Collect all EVSE index in onder to check later if we have a thing for a dissapeard index.
|
||||
indexList.append(evseInfo.index);
|
||||
|
||||
// Check if we already have a child device for this index
|
||||
bool alreadyAdded = false;
|
||||
foreach (Thing *childThing, myThings().filterByParentId(thing->id())) {
|
||||
if (childThing->paramValue("index").toInt() == evseInfo.index) {
|
||||
qCDebug(dcEverest()) << "-> Found already added charger with index" << evseInfo.index;
|
||||
alreadyAdded = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!alreadyAdded) {
|
||||
if (evseInfo.supportedEnergyTransferModes.contains(EverestJsonRpcClient::EnergyTransferModeDC)
|
||||
|| evseInfo.supportedEnergyTransferModes.contains(EverestJsonRpcClient::EnergyTransferModeDC_ACDP)
|
||||
|| evseInfo.supportedEnergyTransferModes.contains(EverestJsonRpcClient::EnergyTransferModeDC_ACDP_BPT)
|
||||
|| evseInfo.supportedEnergyTransferModes.contains(EverestJsonRpcClient::EnergyTransferModeDC_BPT)
|
||||
|| evseInfo.supportedEnergyTransferModes.contains(EverestJsonRpcClient::EnergyTransferModeDC_combo_core)
|
||||
|| evseInfo.supportedEnergyTransferModes.contains(EverestJsonRpcClient::EnergyTransferModeDC_core)
|
||||
|| evseInfo.supportedEnergyTransferModes.contains(EverestJsonRpcClient::EnergyTransferModeDC_extended)
|
||||
|| evseInfo.supportedEnergyTransferModes.contains(EverestJsonRpcClient::EnergyTransferModeDC_unique)) {
|
||||
|
||||
qCDebug(dcEverest()) << "-> Adding new discovered DC charger on" << connection->client()->serverUrl();
|
||||
ThingDescriptor descriptor(everestChargerDcThingClassId, evseInfo.id, evseInfo.description, thing->id());
|
||||
descriptor.setParams(ParamList() << Param(everestChargerDcThingIndexParamTypeId, evseInfo.index));
|
||||
descriptors.append(descriptor);
|
||||
} else {
|
||||
// If not a DC charger, default to AC
|
||||
qCDebug(dcEverest()) << "-> Adding new discovered AC charger on" << connection->client()->serverUrl();
|
||||
ThingDescriptor descriptor(withRfid ? everestChargerAcRfidThingClassId : everestChargerAcThingClassId,
|
||||
evseInfo.id, evseInfo.description, thing->id());
|
||||
descriptor.setParams(ParamList() << Param(withRfid ? everestChargerAcRfidThingIndexParamTypeId : everestChargerAcThingIndexParamTypeId, evseInfo.index));
|
||||
descriptors.append(descriptor);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check if we have things for an index which does not exist for this connection any more
|
||||
QList<ThingId> thingIdsToRemove;
|
||||
foreach (Thing *childThing, myThings().filterByParentId(thing->id())) {
|
||||
if (childThing->paramValue("index").toInt() == evseInfo.index) {
|
||||
qCDebug(dcEverest()) << "-> Found already added charger with index" << evseInfo.index;
|
||||
alreadyAdded = true;
|
||||
break;
|
||||
int thingIndex = childThing->paramValue("index").toInt();
|
||||
if (!indexList.contains(thingIndex)) {
|
||||
qCDebug(dcEverest()) << "Removing thing" << childThing->name() << "because the index" << thingIndex << "does not exist any more on this connection.";
|
||||
emit autoThingDisappeared(childThing->id());
|
||||
}
|
||||
}
|
||||
|
||||
if (!alreadyAdded) {
|
||||
if (evseInfo.supportedEnergyTransferModes.contains(EverestJsonRpcClient::EnergyTransferModeDC)
|
||||
|| evseInfo.supportedEnergyTransferModes.contains(EverestJsonRpcClient::EnergyTransferModeDC_ACDP)
|
||||
|| evseInfo.supportedEnergyTransferModes.contains(EverestJsonRpcClient::EnergyTransferModeDC_ACDP_BPT)
|
||||
|| evseInfo.supportedEnergyTransferModes.contains(EverestJsonRpcClient::EnergyTransferModeDC_BPT)
|
||||
|| evseInfo.supportedEnergyTransferModes.contains(EverestJsonRpcClient::EnergyTransferModeDC_combo_core)
|
||||
|| evseInfo.supportedEnergyTransferModes.contains(EverestJsonRpcClient::EnergyTransferModeDC_core)
|
||||
|| evseInfo.supportedEnergyTransferModes.contains(EverestJsonRpcClient::EnergyTransferModeDC_extended)
|
||||
|| evseInfo.supportedEnergyTransferModes.contains(EverestJsonRpcClient::EnergyTransferModeDC_unique)) {
|
||||
|
||||
qCDebug(dcEverest()) << "-> Adding new discovered DC charger on" << connection->client()->serverUrl();
|
||||
ThingDescriptor descriptor(everestChargerDcThingClassId, evseInfo.id, evseInfo.description, thing->id());
|
||||
descriptor.setParams(ParamList() << Param(everestChargerDcThingIndexParamTypeId, evseInfo.index));
|
||||
descriptors.append(descriptor);
|
||||
} else {
|
||||
// If not a DC charger, default to AC
|
||||
qCDebug(dcEverest()) << "-> Adding new discovered AC charger on" << connection->client()->serverUrl();
|
||||
ThingDescriptor descriptor(everestChargerAcThingClassId, evseInfo.id, evseInfo.description, thing->id());
|
||||
descriptor.setParams(ParamList() << Param(everestChargerAcThingIndexParamTypeId, evseInfo.index));
|
||||
descriptors.append(descriptor);
|
||||
}
|
||||
if (!descriptors.isEmpty()) {
|
||||
emit autoThingsAppeared(descriptors);
|
||||
}
|
||||
}
|
||||
|
||||
// Check if we have things for an index which does not exist for this connection any more
|
||||
QList<ThingId> thingIdsToRemove;
|
||||
foreach (Thing *childThing, myThings().filterByParentId(thing->id())) {
|
||||
int thingIndex = childThing->paramValue("index").toInt();
|
||||
if (!indexList.contains(thingIndex)) {
|
||||
qCDebug(dcEverest()) << "Removing thing" << childThing->name() << "because the index" << thingIndex << "does not exist any more on this connection.";
|
||||
emit autoThingDisappeared(childThing->id());
|
||||
}
|
||||
}
|
||||
|
||||
if (!descriptors.isEmpty()) {
|
||||
emit autoThingsAppeared(descriptors);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@ -474,7 +511,7 @@ void IntegrationPluginEverest::setupThing(ThingSetupInfo *info)
|
||||
|
||||
connection->start();
|
||||
return;
|
||||
} else if (thing->thingClassId() == everestChargerAcThingClassId || thing->thingClassId() == everestChargerDcThingClassId) {
|
||||
} else if (thing->thingClassId() == everestChargerAcThingClassId || thing->thingClassId() == everestChargerAcRfidThingClassId || thing->thingClassId() == everestChargerDcThingClassId) {
|
||||
|
||||
Thing *parentThing = myThings().findById(thing->parentId());
|
||||
EverestConnection *connection = m_everstConnections.value(parentThing);
|
||||
@ -486,12 +523,30 @@ void IntegrationPluginEverest::setupThing(ThingSetupInfo *info)
|
||||
info->finish(Thing::ThingErrorNoError);
|
||||
|
||||
connection->addThing(thing);
|
||||
QTimer::singleShot(0, this, &IntegrationPluginEverest::updateRfidAvailability);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
void IntegrationPluginEverest::executeAction(ThingActionInfo *info)
|
||||
{
|
||||
if (info->thing()->thingClassId() == everestChargerAcRfidThingClassId) {
|
||||
const auto actionId = info->action().actionTypeId();
|
||||
if (actionId == everestChargerAcRfidRfidEnrollmentActiveActionTypeId) {
|
||||
info->thing()->setStateValue("rfidEnrollmentActive",
|
||||
info->action().paramValue(everestChargerAcRfidRfidEnrollmentActiveActionRfidEnrollmentActiveParamTypeId));
|
||||
info->finish(Thing::ThingErrorNoError);
|
||||
return;
|
||||
}
|
||||
if (actionId == everestChargerAcRfidTagAcceptedActionTypeId || actionId == everestChargerAcRfidTagRejectedActionTypeId) {
|
||||
qCInfo(dcEverest()) << "RFID tag"
|
||||
<< (actionId == everestChargerAcRfidTagAcceptedActionTypeId ? "accepted" : "rejected")
|
||||
<< "for" << info->thing()->name() << "- charger feedback RPC is not yet available.";
|
||||
info->thing()->setStateValue("rfidEnrollmentActive", false);
|
||||
info->finish(Thing::ThingErrorNoError);
|
||||
return;
|
||||
}
|
||||
}
|
||||
qCDebug(dcEverest()) << "Executing action for thing" << info->thing()
|
||||
<< info->action().actionTypeId().toString() << info->action().params();
|
||||
|
||||
@ -543,7 +598,7 @@ void IntegrationPluginEverest::executeAction(ThingActionInfo *info)
|
||||
}
|
||||
|
||||
return;
|
||||
} else if (info->thing()->thingClassId() == everestChargerAcThingClassId) {
|
||||
} else if (info->thing()->thingClassId() == everestChargerAcThingClassId || info->thing()->thingClassId() == everestChargerAcRfidThingClassId) {
|
||||
Thing *thing = info->thing();
|
||||
Thing *parentThing = myThings().findById(thing->parentId());
|
||||
EverestConnection *connection = m_everstConnections.value(parentThing);
|
||||
@ -552,7 +607,7 @@ void IntegrationPluginEverest::executeAction(ThingActionInfo *info)
|
||||
return;
|
||||
}
|
||||
|
||||
if (!thing->stateValue(everestChargerAcConnectedStateTypeId).toBool()) {
|
||||
if (!thing->stateValue(QStringLiteral("connected")).toBool()) {
|
||||
info->finish(Thing::ThingErrorHardwareNotAvailable);
|
||||
return;
|
||||
}
|
||||
@ -563,8 +618,8 @@ void IntegrationPluginEverest::executeAction(ThingActionInfo *info)
|
||||
return;
|
||||
}
|
||||
|
||||
if (info->action().actionTypeId() == everestChargerAcPowerActionTypeId) {
|
||||
bool power = info->action().paramValue(everestChargerAcPowerActionPowerParamTypeId).toBool();
|
||||
if (info->action().actionTypeId() == everestChargerAcPowerActionTypeId || info->action().actionTypeId() == everestChargerAcRfidPowerActionTypeId) {
|
||||
bool power = info->action().paramValue(thing->thingClassId() == everestChargerAcRfidThingClassId ? everestChargerAcRfidPowerActionPowerParamTypeId : everestChargerAcPowerActionPowerParamTypeId).toBool();
|
||||
qCDebug(dcEverest()) << "Execute power action" << power;
|
||||
EverestJsonRpcReply *reply = evse->setChargingAllowed(power) ;
|
||||
connect(reply, &EverestJsonRpcReply::finished, reply, &EverestJsonRpcReply::deleteLater);
|
||||
@ -583,11 +638,11 @@ void IntegrationPluginEverest::executeAction(ThingActionInfo *info)
|
||||
return;
|
||||
}
|
||||
|
||||
info->thing()->setStateValue(everestChargerAcCurrentPowerStateTypeId, power);
|
||||
info->thing()->setStateValue(QStringLiteral("power"), power);
|
||||
info->finish(Thing::ThingErrorNoError);
|
||||
});
|
||||
} else if (info->action().actionTypeId() == everestChargerAcMaxChargingCurrentActionTypeId) {
|
||||
double current = info->action().paramValue(everestChargerAcMaxChargingCurrentActionMaxChargingCurrentParamTypeId).toDouble();
|
||||
} else if (info->action().actionTypeId() == everestChargerAcMaxChargingCurrentActionTypeId || info->action().actionTypeId() == everestChargerAcRfidMaxChargingCurrentActionTypeId) {
|
||||
double current = info->action().paramValue(thing->thingClassId() == everestChargerAcRfidThingClassId ? everestChargerAcRfidMaxChargingCurrentActionMaxChargingCurrentParamTypeId : everestChargerAcMaxChargingCurrentActionMaxChargingCurrentParamTypeId).toDouble();
|
||||
qCDebug(dcEverest()) << "Execute action set max charging current" << current << "[A]";
|
||||
EverestJsonRpcReply *reply = evse->setACChargingCurrent(current) ;
|
||||
connect(reply, &EverestJsonRpcReply::finished, reply, &EverestJsonRpcReply::deleteLater);
|
||||
@ -606,11 +661,11 @@ void IntegrationPluginEverest::executeAction(ThingActionInfo *info)
|
||||
return;
|
||||
}
|
||||
|
||||
info->thing()->setStateValue(everestChargerAcMaxChargingCurrentStateTypeId, current);
|
||||
info->thing()->setStateValue(QStringLiteral("maxChargingCurrent"), current);
|
||||
info->finish(Thing::ThingErrorNoError);
|
||||
});
|
||||
} else if (info->action().actionTypeId() == everestChargerAcDesiredPhaseCountActionTypeId) {
|
||||
int phaseCount = info->action().paramValue(everestChargerAcDesiredPhaseCountActionDesiredPhaseCountParamTypeId).toInt();
|
||||
} else if (info->action().actionTypeId() == everestChargerAcDesiredPhaseCountActionTypeId || info->action().actionTypeId() == everestChargerAcRfidDesiredPhaseCountActionTypeId) {
|
||||
int phaseCount = info->action().paramValue(thing->thingClassId() == everestChargerAcRfidThingClassId ? everestChargerAcRfidDesiredPhaseCountActionDesiredPhaseCountParamTypeId : everestChargerAcDesiredPhaseCountActionDesiredPhaseCountParamTypeId).toInt();
|
||||
qCDebug(dcEverest()) << "Execute action set phase count" << phaseCount;
|
||||
EverestJsonRpcReply *reply = evse->setACChargingPhaseCount(phaseCount);
|
||||
connect(reply, &EverestJsonRpcReply::finished, reply, &EverestJsonRpcReply::deleteLater);
|
||||
@ -628,7 +683,7 @@ void IntegrationPluginEverest::executeAction(ThingActionInfo *info)
|
||||
info->finish(Thing::ThingErrorHardwareFailure);
|
||||
return;
|
||||
}
|
||||
info->thing()->setStateValue(everestChargerAcDesiredPhaseCountStateTypeId, phaseCount);
|
||||
info->thing()->setStateValue(QStringLiteral("desiredPhaseCount"), phaseCount);
|
||||
info->finish(Thing::ThingErrorNoError);
|
||||
});
|
||||
}
|
||||
@ -731,7 +786,7 @@ void IntegrationPluginEverest::thingRemoved(Thing *thing)
|
||||
}
|
||||
} else if (thing->thingClassId() == everestConnectionThingClassId) {
|
||||
m_everstConnections.take(thing)->deleteLater();
|
||||
} else if (thing->thingClassId() == everestChargerAcThingClassId || thing->thingClassId() == everestChargerDcThingClassId) {
|
||||
} else if (thing->thingClassId() == everestChargerAcThingClassId || thing->thingClassId() == everestChargerAcRfidThingClassId || thing->thingClassId() == everestChargerDcThingClassId) {
|
||||
Thing *parentThing = myThings().findById(thing->parentId());
|
||||
EverestConnection *connection = m_everstConnections.value(parentThing);
|
||||
if (!connection)
|
||||
@ -739,4 +794,37 @@ void IntegrationPluginEverest::thingRemoved(Thing *thing)
|
||||
|
||||
connection->removeThing(thing);
|
||||
}
|
||||
QTimer::singleShot(0, this, &IntegrationPluginEverest::updateRfidAvailability);
|
||||
}
|
||||
|
||||
Thing *IntegrationPluginEverest::rfidTarget() const
|
||||
{
|
||||
Thing *target = nullptr;
|
||||
int localConnections = 0;
|
||||
for (auto it = m_everstConnections.constBegin(); it != m_everstConnections.constEnd(); ++it) {
|
||||
EverestConnection *connection = it.value();
|
||||
const QString host = connection->client()->serverUrl().host();
|
||||
if (!connection->available() || !(QHostAddress(host).isLoopback() || host.compare("localhost", Qt::CaseInsensitive) == 0))
|
||||
continue;
|
||||
if (++localConnections > 1)
|
||||
return nullptr;
|
||||
if (!everestRfidEligible(connection->client()->serverUrl(), connection->client()->evseInfos()))
|
||||
continue;
|
||||
for (Thing *child : myThings().filterByParentId(it.key()->id())) {
|
||||
if (child->thingClassId() != everestChargerAcRfidThingClassId
|
||||
|| child->paramValue("index").toInt() != connection->client()->evseInfos().first().index)
|
||||
continue;
|
||||
if (target)
|
||||
return nullptr;
|
||||
target = child;
|
||||
}
|
||||
}
|
||||
return target;
|
||||
}
|
||||
|
||||
void IntegrationPluginEverest::updateRfidAvailability()
|
||||
{
|
||||
Thing *target = rfidTarget();
|
||||
for (Thing *thing : myThings().filterByThingClassId(everestChargerAcRfidThingClassId))
|
||||
thing->setStateValue("rfidAvailable", thing == target && m_rfidProvider->ready());
|
||||
}
|
||||
|
||||
@ -30,6 +30,7 @@
|
||||
|
||||
#include "mqtt/everestmqttclient.h"
|
||||
#include "jsonrpc/everestconnection.h"
|
||||
#include "rfid/rfidproviderclient.h"
|
||||
|
||||
#include <mqttclient.h>
|
||||
|
||||
@ -60,6 +61,9 @@ private:
|
||||
QHash<Thing *, EverestMqttClient *> m_thingClients;
|
||||
|
||||
QHash<Thing *, EverestConnection *> m_everstConnections;
|
||||
RfidProviderClient *m_rfidProvider = nullptr;
|
||||
Thing *rfidTarget() const;
|
||||
void updateRfidAvailability();
|
||||
};
|
||||
|
||||
#endif // INTEGRATIONPLUGINEVEREST_H
|
||||
|
||||
@ -247,6 +247,313 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "everestChargerAcRfid",
|
||||
"displayName": "EVerest AC Charger with RFID",
|
||||
"id": "d1b2d417-bf9d-4233-bab4-f1f59c227d24",
|
||||
"setupMethod": "JustAdd",
|
||||
"createMethods": [
|
||||
"auto"
|
||||
],
|
||||
"interfaces": [
|
||||
"evchargerac",
|
||||
"smartmeterconsumer",
|
||||
"connectable",
|
||||
"rfid"
|
||||
],
|
||||
"paramTypes": [
|
||||
{
|
||||
"id": "d0be2ecd-3cc0-489f-9ad1-93b118563085",
|
||||
"name": "index",
|
||||
"displayName": "Index",
|
||||
"type": "uint",
|
||||
"defaultValue": 0
|
||||
}
|
||||
],
|
||||
"settingsTypes": [],
|
||||
"stateTypes": [
|
||||
{
|
||||
"id": "ff8c4bda-6e29-400e-ab6e-da991d8a80d0",
|
||||
"name": "connected",
|
||||
"displayName": "Connected",
|
||||
"type": "bool",
|
||||
"cached": false,
|
||||
"defaultValue": false
|
||||
},
|
||||
{
|
||||
"id": "ea3d96f9-8657-4c34-bf70-91c026314782",
|
||||
"name": "power",
|
||||
"displayName": "Charging enabled",
|
||||
"displayNameAction": "Enable or disable charging",
|
||||
"type": "bool",
|
||||
"defaultValue": false,
|
||||
"writable": true
|
||||
},
|
||||
{
|
||||
"id": "1777220f-27f5-458c-a9ac-90383e27b99c",
|
||||
"name": "maxChargingCurrent",
|
||||
"displayName": "Maximum charging current",
|
||||
"displayNameAction": "Set maximum charging current",
|
||||
"type": "double",
|
||||
"unit": "Ampere",
|
||||
"defaultValue": 6,
|
||||
"minValue": 6,
|
||||
"maxValue": 32,
|
||||
"stepSize": 0.01,
|
||||
"writable": true
|
||||
},
|
||||
{
|
||||
"id": "ec1864b9-0dd4-40c2-b23b-750ebdbda75d",
|
||||
"name": "pluggedIn",
|
||||
"displayName": "Plugged in",
|
||||
"type": "bool",
|
||||
"defaultValue": false
|
||||
},
|
||||
{
|
||||
"id": "5e8d075f-4bc4-4531-aa5c-2112755dc213",
|
||||
"name": "charging",
|
||||
"displayName": "Charging",
|
||||
"type": "bool",
|
||||
"defaultValue": false
|
||||
},
|
||||
{
|
||||
"id": "15468c25-f7be-42f1-933e-af5c753478a3",
|
||||
"name": "phaseCount",
|
||||
"displayName": "Active phases",
|
||||
"type": "uint",
|
||||
"minValue": 1,
|
||||
"maxValue": 3,
|
||||
"defaultValue": 1
|
||||
},
|
||||
{
|
||||
"id": "5fb08851-36fd-43f5-b202-eb888304dad2",
|
||||
"name": "desiredPhaseCount",
|
||||
"displayName": "Desired phase count",
|
||||
"displayNameAction": "Set desired phase count",
|
||||
"type": "uint",
|
||||
"minValue": 1,
|
||||
"maxValue": 3,
|
||||
"possibleValues": [
|
||||
1,
|
||||
3
|
||||
],
|
||||
"writable": true,
|
||||
"defaultValue": 3
|
||||
},
|
||||
{
|
||||
"id": "25136724-fd6c-42c8-b88a-7b62ccfde4ba",
|
||||
"name": "state",
|
||||
"displayName": "State",
|
||||
"type": "QString",
|
||||
"defaultValue": "",
|
||||
"cached": false
|
||||
},
|
||||
{
|
||||
"id": "f20ce7c1-9089-4233-b7d9-ff9d9d26d30b",
|
||||
"name": "totalEnergyConsumed",
|
||||
"displayName": "Total energy",
|
||||
"type": "double",
|
||||
"unit": "KiloWattHour",
|
||||
"defaultValue": 0,
|
||||
"cached": true
|
||||
},
|
||||
{
|
||||
"id": "fa51df44-fb7b-4aaa-857c-cfdd6ca5cbf3",
|
||||
"name": "sessionEnergy",
|
||||
"displayName": "Session energy",
|
||||
"type": "double",
|
||||
"unit": "KiloWattHour",
|
||||
"defaultValue": 0,
|
||||
"suggestLogging": true
|
||||
},
|
||||
{
|
||||
"id": "82f4a7ec-96fd-4c02-bba8-b46102b6d8b8",
|
||||
"name": "currentPower",
|
||||
"displayName": "Current power",
|
||||
"type": "double",
|
||||
"unit": "Watt",
|
||||
"defaultValue": 0,
|
||||
"cached": false
|
||||
},
|
||||
{
|
||||
"id": "1201f1a2-1486-4425-a022-f1f106503c11",
|
||||
"name": "currentPowerPhaseA",
|
||||
"displayName": "Current power phase A",
|
||||
"displayNameEvent": "Current power phase A changed",
|
||||
"type": "double",
|
||||
"unit": "Watt",
|
||||
"defaultValue": 0,
|
||||
"cached": false
|
||||
},
|
||||
{
|
||||
"id": "8ae52af8-7d62-4552-b2b7-6c0e4cc9ce01",
|
||||
"name": "currentPowerPhaseB",
|
||||
"displayName": "Current power phase B",
|
||||
"displayNameEvent": "Current power phase B changed",
|
||||
"type": "double",
|
||||
"unit": "Watt",
|
||||
"defaultValue": 0,
|
||||
"cached": false
|
||||
},
|
||||
{
|
||||
"id": "41c0cb18-bee3-43f8-92c1-8e9637e5f935",
|
||||
"name": "currentPowerPhaseC",
|
||||
"displayName": "Current power phase C",
|
||||
"displayNameEvent": "Current power phase C changed",
|
||||
"type": "double",
|
||||
"unit": "Watt",
|
||||
"defaultValue": 0,
|
||||
"cached": false
|
||||
},
|
||||
{
|
||||
"id": "0abaf10b-a7f5-4a20-94ad-f3f544e8adf1",
|
||||
"name": "currentPhaseA",
|
||||
"displayName": "Phase A current",
|
||||
"displayNameEvent": "Phase A current changed",
|
||||
"type": "double",
|
||||
"unit": "Ampere",
|
||||
"defaultValue": 0,
|
||||
"cached": false
|
||||
},
|
||||
{
|
||||
"id": "a374a8a7-ebe9-47a4-9552-dc477efcd61f",
|
||||
"name": "currentPhaseB",
|
||||
"displayName": "Phase B current",
|
||||
"displayNameEvent": "Phase B current changed",
|
||||
"type": "double",
|
||||
"unit": "Ampere",
|
||||
"defaultValue": 0,
|
||||
"cached": false
|
||||
},
|
||||
{
|
||||
"id": "c84e3184-a98d-40c7-9f46-61ccd92562a8",
|
||||
"name": "currentPhaseC",
|
||||
"displayName": "Phase C current",
|
||||
"displayNameEvent": "Phase C current changed",
|
||||
"type": "double",
|
||||
"unit": "Ampere",
|
||||
"defaultValue": 0,
|
||||
"cached": false
|
||||
},
|
||||
{
|
||||
"id": "e3bfd7ea-b790-4e2a-8086-9a05ea9eccf5",
|
||||
"name": "voltagePhaseA",
|
||||
"displayName": "Phase A voltage",
|
||||
"displayNameEvent": "Phase A volatage changed",
|
||||
"type": "double",
|
||||
"unit": "Volt",
|
||||
"defaultValue": 0,
|
||||
"cached": false
|
||||
},
|
||||
{
|
||||
"id": "5ed8a2db-93ca-4e4b-9b5e-3452b6677285",
|
||||
"name": "voltagePhaseB",
|
||||
"displayName": "Phase B voltage",
|
||||
"displayNameEvent": "Phase B voltage changed",
|
||||
"type": "double",
|
||||
"unit": "Volt",
|
||||
"defaultValue": 0,
|
||||
"cached": false
|
||||
},
|
||||
{
|
||||
"id": "aec3afdc-37d3-4ca8-b7b5-b1454189b082",
|
||||
"name": "voltagePhaseC",
|
||||
"displayName": "Phase C voltage",
|
||||
"displayNameEvent": "Phase C voltage changed",
|
||||
"type": "double",
|
||||
"unit": "Volt",
|
||||
"defaultValue": 0,
|
||||
"cached": false
|
||||
},
|
||||
{
|
||||
"id": "c8bf937f-86bf-4fb1-9a21-14c204942977",
|
||||
"name": "temperature",
|
||||
"displayName": "Temperature",
|
||||
"type": "double",
|
||||
"unit": "DegreeCelsius",
|
||||
"defaultValue": 0,
|
||||
"cached": false
|
||||
},
|
||||
{
|
||||
"id": "ecff27d3-451b-4d4b-b018-d7779d132d81",
|
||||
"name": "fanSpeed",
|
||||
"displayName": "Fan speed",
|
||||
"type": "double",
|
||||
"unit": "Rpm",
|
||||
"defaultValue": 0,
|
||||
"cached": false
|
||||
},
|
||||
{
|
||||
"id": "f24fc1ef-5827-4f9e-a280-6db07160120e",
|
||||
"name": "rfidAvailable",
|
||||
"displayName": "RFID reader available",
|
||||
"type": "bool",
|
||||
"cached": false,
|
||||
"defaultValue": false
|
||||
},
|
||||
{
|
||||
"id": "c9cbaa00-514c-4832-ba24-1870a1fee382",
|
||||
"name": "authorizedUsername",
|
||||
"displayName": "Authorized username",
|
||||
"type": "QString",
|
||||
"cached": false,
|
||||
"defaultValue": ""
|
||||
},
|
||||
{
|
||||
"id": "8c930a59-656b-4036-b0de-4c77597d4d75",
|
||||
"name": "authorizedDisplayName",
|
||||
"displayName": "Authorized display name",
|
||||
"type": "QString",
|
||||
"cached": false,
|
||||
"defaultValue": ""
|
||||
},
|
||||
{
|
||||
"id": "3bf9bb07-4270-4665-a15c-78efa9b28ed4",
|
||||
"name": "authorizedTagHash",
|
||||
"displayName": "Authorized tag hash",
|
||||
"type": "QString",
|
||||
"cached": false,
|
||||
"defaultValue": ""
|
||||
},
|
||||
{
|
||||
"id": "fc752205-ca01-45c2-9448-13b6325be6b5",
|
||||
"name": "rfidEnrollmentActive",
|
||||
"displayName": "RFID enrollment active",
|
||||
"displayNameAction": "Set RFID enrollment mode",
|
||||
"type": "bool",
|
||||
"writable": true,
|
||||
"cached": false,
|
||||
"defaultValue": false
|
||||
}
|
||||
],
|
||||
"actionTypes": [
|
||||
{
|
||||
"id": "dc9cd114-6066-4c11-9282-f2a80963f631",
|
||||
"name": "tagAccepted",
|
||||
"displayName": "Accept RFID tag"
|
||||
},
|
||||
{
|
||||
"id": "915e9030-9dcd-448d-b8cc-cbbb0b2d7957",
|
||||
"name": "tagRejected",
|
||||
"displayName": "Reject RFID tag"
|
||||
}
|
||||
],
|
||||
"eventTypes": [
|
||||
{
|
||||
"id": "43c6e179-a513-4ad8-a88a-c0a4dbda5a3d",
|
||||
"name": "tagDetected",
|
||||
"displayName": "RFID tag detected",
|
||||
"paramTypes": [
|
||||
{
|
||||
"id": "90d55a9d-81a2-4791-9e57-3f6f9efbff75",
|
||||
"name": "code",
|
||||
"displayName": "Tag code",
|
||||
"type": "QString"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "everestChargerAc",
|
||||
"displayName": "EVerest AC Charger",
|
||||
@ -745,4 +1052,3 @@
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
|
||||
@ -211,18 +211,18 @@ void EverestEvse::initialize()
|
||||
|
||||
void EverestEvse::processEvseStatus()
|
||||
{
|
||||
if (m_thing->thingClassId() == everestChargerAcThingClassId) {
|
||||
m_thing->setStateValue(everestChargerAcPowerStateTypeId, m_evseStatus.chargingAllowed);
|
||||
m_thing->setStateValue(everestChargerAcSessionEnergyStateTypeId, m_evseStatus.chargedEnergyWh / 1000.0);
|
||||
m_thing->setStateValue(everestChargerAcStateStateTypeId, m_evseStatus.evseStateString);
|
||||
m_thing->setStateValue(everestChargerAcChargingStateTypeId, m_evseStatus.evseState == EverestJsonRpcClient::EvseStateCharging);
|
||||
m_thing->setStateValue(everestChargerAcPluggedInStateTypeId, m_evseStatus.evseState != EverestJsonRpcClient::EvseStateUnplugged);
|
||||
if ((m_thing->thingClassId() == everestChargerAcThingClassId || m_thing->thingClassId() == everestChargerAcRfidThingClassId)) {
|
||||
m_thing->setStateValue(QStringLiteral("power"), m_evseStatus.chargingAllowed);
|
||||
m_thing->setStateValue(QStringLiteral("sessionEnergy"), m_evseStatus.chargedEnergyWh / 1000.0);
|
||||
m_thing->setStateValue(QStringLiteral("state"), m_evseStatus.evseStateString);
|
||||
m_thing->setStateValue(QStringLiteral("charging"), m_evseStatus.evseState == EverestJsonRpcClient::EvseStateCharging);
|
||||
m_thing->setStateValue(QStringLiteral("pluggedIn"), m_evseStatus.evseState != EverestJsonRpcClient::EvseStateUnplugged);
|
||||
|
||||
m_thing->setStateValue(everestChargerAcPhaseCountStateTypeId, m_evseStatus.acChargeStatus.activePhaseCount);
|
||||
m_thing->setStateValue(everestChargerAcDesiredPhaseCountStateTypeId, m_evseStatus.acChargeStatus.activePhaseCount);
|
||||
m_thing->setStateValue(QStringLiteral("phaseCount"), m_evseStatus.acChargeStatus.activePhaseCount);
|
||||
m_thing->setStateValue(QStringLiteral("desiredPhaseCount"), m_evseStatus.acChargeStatus.activePhaseCount);
|
||||
|
||||
if (m_evseStatus.acChargeParameters.maxCurrent > 0)
|
||||
m_thing->setStateValue(everestChargerAcMaxChargingCurrentStateTypeId, m_evseStatus.acChargeParameters.maxCurrent);
|
||||
m_thing->setStateValue(QStringLiteral("maxChargingCurrent"), m_evseStatus.acChargeParameters.maxCurrent);
|
||||
|
||||
} else if (m_thing->thingClassId() == everestChargerDcThingClassId) {
|
||||
|
||||
@ -248,40 +248,40 @@ void EverestEvse::processEvseStatus()
|
||||
|
||||
void EverestEvse::processHardwareCapabilities()
|
||||
{
|
||||
if (m_thing->thingClassId() == everestChargerAcThingClassId) {
|
||||
if ((m_thing->thingClassId() == everestChargerAcThingClassId || m_thing->thingClassId() == everestChargerAcRfidThingClassId)) {
|
||||
if (!m_hardwareCapabilities.phaseSwitchDuringCharging) {
|
||||
// Only option left is set the desired phase count to 3, force that value
|
||||
m_thing->setStatePossibleValues(everestChargerAcDesiredPhaseCountStateTypeId, { 3 });
|
||||
m_thing->setStateValue(everestChargerAcDesiredPhaseCountStateTypeId, 3);
|
||||
m_thing->setStateValue(everestChargerAcPhaseCountStateTypeId, 3);
|
||||
m_thing->setStatePossibleValues(QStringLiteral("desiredPhaseCount"), { 3 });
|
||||
m_thing->setStateValue(QStringLiteral("desiredPhaseCount"), 3);
|
||||
m_thing->setStateValue(QStringLiteral("phaseCount"), 3);
|
||||
} else {
|
||||
m_thing->setStatePossibleValues(everestChargerAcDesiredPhaseCountStateTypeId, { 1, 3 });
|
||||
m_thing->setStatePossibleValues(QStringLiteral("desiredPhaseCount"), { 1, 3 });
|
||||
}
|
||||
|
||||
m_thing->setStateMaxValue(everestChargerAcMaxChargingCurrentStateTypeId, m_hardwareCapabilities.maxCurrentImport);
|
||||
m_thing->setStateMinValue(everestChargerAcMaxChargingCurrentStateTypeId, m_hardwareCapabilities.minCurrentImport == 0 ? 6 : m_hardwareCapabilities.minCurrentImport);
|
||||
m_thing->setStateMaxValue(QStringLiteral("maxChargingCurrent"), m_hardwareCapabilities.maxCurrentImport);
|
||||
m_thing->setStateMinValue(QStringLiteral("maxChargingCurrent"), m_hardwareCapabilities.minCurrentImport == 0 ? 6 : m_hardwareCapabilities.minCurrentImport);
|
||||
}
|
||||
}
|
||||
|
||||
void EverestEvse::processMeterData()
|
||||
{
|
||||
if (m_thing->thingClassId() == everestChargerAcThingClassId) {
|
||||
if ((m_thing->thingClassId() == everestChargerAcThingClassId || m_thing->thingClassId() == everestChargerAcRfidThingClassId)) {
|
||||
|
||||
m_thing->setStateValue(everestChargerAcCurrentPowerStateTypeId, m_meterData.powerTotal);
|
||||
m_thing->setStateValue(QStringLiteral("currentPower"), m_meterData.powerTotal);
|
||||
|
||||
m_thing->setStateValue(everestChargerAcCurrentPowerPhaseAStateTypeId, m_meterData.powerL1);
|
||||
m_thing->setStateValue(everestChargerAcCurrentPowerPhaseBStateTypeId, m_meterData.powerL2);
|
||||
m_thing->setStateValue(everestChargerAcCurrentPowerPhaseCStateTypeId, m_meterData.powerL3);
|
||||
m_thing->setStateValue(QStringLiteral("currentPowerPhaseA"), m_meterData.powerL1);
|
||||
m_thing->setStateValue(QStringLiteral("currentPowerPhaseB"), m_meterData.powerL2);
|
||||
m_thing->setStateValue(QStringLiteral("currentPowerPhaseC"), m_meterData.powerL3);
|
||||
|
||||
m_thing->setStateValue(everestChargerAcCurrentPhaseAStateTypeId, m_meterData.currentL1);
|
||||
m_thing->setStateValue(everestChargerAcCurrentPhaseBStateTypeId, m_meterData.currentL2);
|
||||
m_thing->setStateValue(everestChargerAcCurrentPhaseCStateTypeId, m_meterData.currentL3);
|
||||
m_thing->setStateValue(QStringLiteral("currentPhaseA"), m_meterData.currentL1);
|
||||
m_thing->setStateValue(QStringLiteral("currentPhaseB"), m_meterData.currentL2);
|
||||
m_thing->setStateValue(QStringLiteral("currentPhaseC"), m_meterData.currentL3);
|
||||
|
||||
m_thing->setStateValue(everestChargerAcVoltagePhaseAStateTypeId, m_meterData.voltageL1);
|
||||
m_thing->setStateValue(everestChargerAcVoltagePhaseBStateTypeId, m_meterData.voltageL2);
|
||||
m_thing->setStateValue(everestChargerAcVoltagePhaseCStateTypeId, m_meterData.voltageL3);
|
||||
m_thing->setStateValue(QStringLiteral("voltagePhaseA"), m_meterData.voltageL1);
|
||||
m_thing->setStateValue(QStringLiteral("voltagePhaseB"), m_meterData.voltageL2);
|
||||
m_thing->setStateValue(QStringLiteral("voltagePhaseC"), m_meterData.voltageL3);
|
||||
|
||||
m_thing->setStateValue(everestChargerAcTotalEnergyConsumedStateTypeId, m_meterData.energyImportedTotal / 1000.0);
|
||||
m_thing->setStateValue(QStringLiteral("totalEnergyConsumed"), m_meterData.energyImportedTotal / 1000.0);
|
||||
} else if (m_thing->thingClassId() == everestChargerDcThingClassId) {
|
||||
|
||||
m_thing->setStateValue(everestChargerDcCurrentPowerStateTypeId, m_meterData.powerTotal);
|
||||
|
||||
34
everest/rfid/everestrfidutils.h
Normal file
34
everest/rfid/everestrfidutils.h
Normal file
@ -0,0 +1,34 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
// Copyright (C) 2026, chargebyte austria GmbH
|
||||
|
||||
#ifndef EVERESTRFIDUTILS_H
|
||||
#define EVERESTRFIDUTILS_H
|
||||
|
||||
#include "jsonrpc/everestjsonrpcclient.h"
|
||||
#include <QHostAddress>
|
||||
|
||||
inline bool everestRfidEligible(const QUrl &url, const QList<EverestJsonRpcClient::EVSEInfo> &evses)
|
||||
{
|
||||
if (!(QHostAddress(url.host()).isLoopback() || url.host().compare("localhost", Qt::CaseInsensitive) == 0)
|
||||
|| evses.size() != 1 || evses.first().availableConnectors.size() != 1)
|
||||
return false;
|
||||
bool ac = false;
|
||||
for (auto mode : evses.first().supportedEnergyTransferModes) {
|
||||
switch (mode) {
|
||||
case EverestJsonRpcClient::EnergyTransferModeAC_single_phase_core:
|
||||
case EverestJsonRpcClient::EnergyTransferModeAC_two_phase:
|
||||
case EverestJsonRpcClient::EnergyTransferModeAC_three_phase_core:
|
||||
case EverestJsonRpcClient::EnergyTransferModeAC_BPT:
|
||||
case EverestJsonRpcClient::EnergyTransferModeAC_BPT_DER:
|
||||
case EverestJsonRpcClient::EnergyTransferModeAC_DER:
|
||||
ac = true;
|
||||
break;
|
||||
default:
|
||||
// Unknown modes and any DC capability cannot safely select this class.
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return ac;
|
||||
}
|
||||
|
||||
#endif // EVERESTRFIDUTILS_H
|
||||
176
everest/rfid/rfidproviderclient.cpp
Normal file
176
everest/rfid/rfidproviderclient.cpp
Normal file
@ -0,0 +1,176 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
// Copyright (C) 2026, chargebyte austria GmbH
|
||||
|
||||
#include "rfidproviderclient.h"
|
||||
|
||||
#include <QDBusPendingCallWatcher>
|
||||
#include <QDBusPendingReply>
|
||||
#include <QDBusServiceWatcher>
|
||||
#include <QXmlStreamReader>
|
||||
|
||||
namespace {
|
||||
const QString service = QStringLiteral("io.nymea.RfidProvider");
|
||||
const QString path = QStringLiteral("/io/nymea/RfidProvider");
|
||||
const QString interface = QStringLiteral("io.nymea.RfidProvider1");
|
||||
const QString properties = QStringLiteral("org.freedesktop.DBus.Properties");
|
||||
|
||||
bool compatible(const QString &xml)
|
||||
{
|
||||
QXmlStreamReader reader(xml);
|
||||
bool foundSignal = false;
|
||||
QMap<QString, QString> propertyTypes;
|
||||
while (reader.readNextStartElement()) {
|
||||
if (reader.name() == QLatin1String("node"))
|
||||
continue;
|
||||
if (reader.name() != QLatin1String("interface") || reader.attributes().value("name") != interface) {
|
||||
reader.skipCurrentElement();
|
||||
continue;
|
||||
}
|
||||
while (reader.readNextStartElement()) {
|
||||
const auto attributes = reader.attributes();
|
||||
if (reader.name() == QLatin1String("property")) {
|
||||
const QString access = attributes.value("access").toString();
|
||||
if (access == QLatin1String("read") || access == QLatin1String("readwrite"))
|
||||
propertyTypes.insert(attributes.value("name").toString(), attributes.value("type").toString());
|
||||
reader.skipCurrentElement();
|
||||
} else if (reader.name() == QLatin1String("signal") && attributes.value("name") == QLatin1String("tagDetected")) {
|
||||
QString signature;
|
||||
while (reader.readNextStartElement()) {
|
||||
if (reader.name() == QLatin1String("arg"))
|
||||
signature += reader.attributes().value("type").toString();
|
||||
reader.skipCurrentElement();
|
||||
}
|
||||
foundSignal = signature == QLatin1String("sa{sv}");
|
||||
} else {
|
||||
reader.skipCurrentElement();
|
||||
}
|
||||
}
|
||||
}
|
||||
return !reader.hasError() && foundSignal && propertyTypes.value("Backend") == QLatin1String("s")
|
||||
&& propertyTypes.value("Ready") == QLatin1String("b")
|
||||
&& propertyTypes.value("SupportsPresence") == QLatin1String("b")
|
||||
&& propertyTypes.value("LastError") == QLatin1String("s");
|
||||
}
|
||||
}
|
||||
|
||||
RfidProviderClient::RfidProviderClient(QObject *parent, const QDBusConnection &connection) :
|
||||
QObject(parent), m_connection(connection)
|
||||
{
|
||||
m_probeTimer.setSingleShot(true);
|
||||
m_probeTimer.setInterval(1500);
|
||||
connect(&m_probeTimer, &QTimer::timeout, this, [this]() { finishProbe(false); });
|
||||
auto watcher = new QDBusServiceWatcher(service, m_connection, QDBusServiceWatcher::WatchForOwnerChange, this);
|
||||
connect(watcher, &QDBusServiceWatcher::serviceOwnerChanged, this,
|
||||
[this](const QString &, const QString &, const QString &newOwner) {
|
||||
++m_generation;
|
||||
m_owner.clear();
|
||||
m_available = false;
|
||||
m_ready = false;
|
||||
emit availabilityChanged();
|
||||
if (newOwner.isEmpty()) {
|
||||
finishProbe(false);
|
||||
} else {
|
||||
m_probing = false;
|
||||
probe();
|
||||
}
|
||||
});
|
||||
m_connection.connect(service, path, interface, "tagDetected", this,
|
||||
SLOT(onTagDetected(QString,QVariantMap,QDBusMessage)));
|
||||
m_connection.connect(service, path, properties, "PropertiesChanged", this,
|
||||
SLOT(onPropertiesChanged(QString,QVariantMap,QStringList,QDBusMessage)));
|
||||
}
|
||||
|
||||
bool RfidProviderClient::available() const { return m_available; }
|
||||
bool RfidProviderClient::ready() const { return m_available && m_ready; }
|
||||
|
||||
void RfidProviderClient::probe(std::function<void(bool)> callback)
|
||||
{
|
||||
if (callback)
|
||||
m_callbacks.append(std::move(callback));
|
||||
if (m_probing)
|
||||
return;
|
||||
m_probing = true;
|
||||
const quint64 generation = ++m_generation;
|
||||
m_probeTimer.start();
|
||||
auto request = QDBusMessage::createMethodCall("org.freedesktop.DBus", "/org/freedesktop/DBus",
|
||||
"org.freedesktop.DBus", "GetNameOwner");
|
||||
request << service;
|
||||
auto ownerCall = new QDBusPendingCallWatcher(m_connection.asyncCall(request, 1500), this);
|
||||
connect(ownerCall, &QDBusPendingCallWatcher::finished, this, [this, ownerCall, generation]() {
|
||||
ownerCall->deleteLater();
|
||||
if (!m_probing || generation != m_generation)
|
||||
return;
|
||||
QDBusPendingReply<QString> ownerReply = *ownerCall;
|
||||
if (ownerReply.isError()) {
|
||||
finishProbe(false);
|
||||
return;
|
||||
}
|
||||
m_owner = ownerReply.value();
|
||||
auto request = QDBusMessage::createMethodCall(m_owner, path, "org.freedesktop.DBus.Introspectable", "Introspect");
|
||||
auto introspectCall = new QDBusPendingCallWatcher(m_connection.asyncCall(request, 1500), this);
|
||||
connect(introspectCall, &QDBusPendingCallWatcher::finished, this, [this, introspectCall, generation]() {
|
||||
introspectCall->deleteLater();
|
||||
if (!m_probing || generation != m_generation)
|
||||
return;
|
||||
QDBusPendingReply<QString> reply = *introspectCall;
|
||||
if (reply.isError() || !compatible(reply.value())) {
|
||||
finishProbe(false);
|
||||
return;
|
||||
}
|
||||
auto request = QDBusMessage::createMethodCall(m_owner, path, properties, "GetAll");
|
||||
request << interface;
|
||||
auto propertiesCall = new QDBusPendingCallWatcher(m_connection.asyncCall(request, 1500), this);
|
||||
connect(propertiesCall, &QDBusPendingCallWatcher::finished, this, [this, propertiesCall, generation]() {
|
||||
propertiesCall->deleteLater();
|
||||
if (!m_probing || generation != m_generation)
|
||||
return;
|
||||
QDBusPendingReply<QVariantMap> reply = *propertiesCall;
|
||||
const QVariantMap values = reply.value();
|
||||
const bool valid = !reply.isError() && values.value("Ready").userType() == QMetaType::Bool
|
||||
&& values.value("Backend").userType() == QMetaType::QString
|
||||
&& values.value("SupportsPresence").userType() == QMetaType::Bool
|
||||
&& values.value("LastError").userType() == QMetaType::QString;
|
||||
finishProbe(valid, values.value("Ready").toBool());
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
void RfidProviderClient::finishProbe(bool available, bool ready)
|
||||
{
|
||||
m_probeTimer.stop();
|
||||
m_probing = false;
|
||||
++m_generation;
|
||||
const bool changed = m_available != available || m_ready != ready;
|
||||
m_available = available;
|
||||
m_ready = ready;
|
||||
if (!available)
|
||||
m_owner.clear();
|
||||
if (changed)
|
||||
emit availabilityChanged();
|
||||
const auto callbacks = std::move(m_callbacks);
|
||||
m_callbacks.clear();
|
||||
for (const auto &callback : callbacks)
|
||||
callback(available);
|
||||
}
|
||||
|
||||
void RfidProviderClient::onTagDetected(const QString &code, const QVariantMap &metadata, const QDBusMessage &message)
|
||||
{
|
||||
if (ready() && message.service() == m_owner && !code.isEmpty())
|
||||
emit tagDetected(code, metadata);
|
||||
}
|
||||
|
||||
void RfidProviderClient::onPropertiesChanged(const QString &changedInterface, const QVariantMap &changed,
|
||||
const QStringList &invalidated, const QDBusMessage &message)
|
||||
{
|
||||
if (!m_available || changedInterface != interface || message.service() != m_owner)
|
||||
return;
|
||||
if (invalidated.contains("Ready")) {
|
||||
m_ready = false;
|
||||
emit availabilityChanged();
|
||||
probe();
|
||||
} else if (changed.contains("Ready")) {
|
||||
m_ready = changed.value("Ready").userType() == QMetaType::Bool && changed.value("Ready").toBool();
|
||||
emit availabilityChanged();
|
||||
}
|
||||
}
|
||||
46
everest/rfid/rfidproviderclient.h
Normal file
46
everest/rfid/rfidproviderclient.h
Normal file
@ -0,0 +1,46 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
// Copyright (C) 2026, chargebyte austria GmbH
|
||||
|
||||
#ifndef RFIDPROVIDERCLIENT_H
|
||||
#define RFIDPROVIDERCLIENT_H
|
||||
|
||||
#include <QObject>
|
||||
#include <QDBusConnection>
|
||||
#include <QDBusMessage>
|
||||
#include <QTimer>
|
||||
#include <QVariantMap>
|
||||
#include <functional>
|
||||
|
||||
class RfidProviderClient : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit RfidProviderClient(QObject *parent = nullptr,
|
||||
const QDBusConnection &connection = QDBusConnection::systemBus());
|
||||
bool available() const;
|
||||
bool ready() const;
|
||||
void probe(std::function<void(bool)> callback = {});
|
||||
|
||||
signals:
|
||||
void availabilityChanged();
|
||||
void tagDetected(const QString &code, const QVariantMap &metadata);
|
||||
|
||||
private slots:
|
||||
void onTagDetected(const QString &code, const QVariantMap &metadata, const QDBusMessage &message);
|
||||
void onPropertiesChanged(const QString &interface, const QVariantMap &changed,
|
||||
const QStringList &invalidated, const QDBusMessage &message);
|
||||
|
||||
private:
|
||||
QDBusConnection m_connection;
|
||||
QTimer m_probeTimer;
|
||||
QList<std::function<void(bool)>> m_callbacks;
|
||||
QString m_owner;
|
||||
quint64 m_generation = 0;
|
||||
bool m_probing = false;
|
||||
bool m_available = false;
|
||||
bool m_ready = false;
|
||||
|
||||
void finishProbe(bool available, bool ready = false);
|
||||
};
|
||||
|
||||
#endif // RFIDPROVIDERCLIENT_H
|
||||
16
everest/tests/CMakeLists.txt
Normal file
16
everest/tests/CMakeLists.txt
Normal file
@ -0,0 +1,16 @@
|
||||
cmake_minimum_required(VERSION 3.16)
|
||||
project(everest-rfid-tests LANGUAGES CXX)
|
||||
set(CMAKE_CXX_STANDARD 17)
|
||||
set(CMAKE_AUTOMOC ON)
|
||||
enable_testing()
|
||||
set(NYMEA_TEST_QT_MAJOR_VERSION 6 CACHE STRING "Qt major version used by the standalone tests")
|
||||
find_package(Qt${NYMEA_TEST_QT_MAJOR_VERSION} REQUIRED COMPONENTS Core DBus Test Network WebSockets)
|
||||
find_program(DBUS_RUN_SESSION dbus-run-session REQUIRED)
|
||||
set(NYMEA_SOURCE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/../../../nymea" CACHE PATH "nymea source checkout (headers only)")
|
||||
add_executable(tst_rfidproviderclient tst_rfidproviderclient.cpp ../rfid/rfidproviderclient.cpp)
|
||||
target_include_directories(tst_rfidproviderclient PRIVATE .. "${NYMEA_SOURCE_DIR}/libnymea")
|
||||
target_link_libraries(tst_rfidproviderclient PRIVATE
|
||||
Qt${NYMEA_TEST_QT_MAJOR_VERSION}::Core Qt${NYMEA_TEST_QT_MAJOR_VERSION}::DBus
|
||||
Qt${NYMEA_TEST_QT_MAJOR_VERSION}::Test Qt${NYMEA_TEST_QT_MAJOR_VERSION}::Network
|
||||
Qt${NYMEA_TEST_QT_MAJOR_VERSION}::WebSockets)
|
||||
add_test(NAME everest-rfid COMMAND ${DBUS_RUN_SESSION} -- $<TARGET_FILE:tst_rfidproviderclient>)
|
||||
150
everest/tests/tst_rfidproviderclient.cpp
Normal file
150
everest/tests/tst_rfidproviderclient.cpp
Normal file
@ -0,0 +1,150 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
#include <QtTest>
|
||||
#include <QDBusConnection>
|
||||
#include <QDBusMessage>
|
||||
#include "rfid/rfidproviderclient.h"
|
||||
#include "rfid/everestrfidutils.h"
|
||||
|
||||
class ProviderFixture : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
Q_CLASSINFO("D-Bus Interface", "io.nymea.RfidProvider1")
|
||||
Q_PROPERTY(QString Backend READ backend)
|
||||
Q_PROPERTY(bool Ready READ ready)
|
||||
Q_PROPERTY(bool SupportsPresence READ supportsPresence)
|
||||
Q_PROPERTY(QString LastError READ lastError)
|
||||
public:
|
||||
bool isReady = false;
|
||||
QString backend() const { return "fake"; }
|
||||
bool ready() const { return isReady; }
|
||||
bool supportsPresence() const { return true; }
|
||||
QString lastError() const { return {}; }
|
||||
signals:
|
||||
void tagArrived();
|
||||
void tagDetected(const QString &code, const QVariantMap &metadata);
|
||||
void tagRemoved();
|
||||
};
|
||||
|
||||
class TestRfidProviderClient : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
QDBusConnection bus = QDBusConnection::sessionBus();
|
||||
private slots:
|
||||
void cleanup()
|
||||
{
|
||||
bus.unregisterService("io.nymea.RfidProvider");
|
||||
bus.unregisterObject("/io/nymea/RfidProvider");
|
||||
}
|
||||
|
||||
void absentProvider()
|
||||
{
|
||||
RfidProviderClient client(nullptr, bus);
|
||||
int callbacks = 0;
|
||||
bool result = true;
|
||||
client.probe([&](bool found) { ++callbacks; result = found; });
|
||||
QTRY_COMPARE_WITH_TIMEOUT(callbacks, 1, 2500);
|
||||
QVERIFY(!result);
|
||||
QVERIFY(!client.available());
|
||||
QVERIFY(!client.ready());
|
||||
}
|
||||
|
||||
void incompatibleProvider()
|
||||
{
|
||||
QObject incompatible;
|
||||
QVERIFY(bus.registerObject("/io/nymea/RfidProvider", &incompatible, QDBusConnection::ExportAllContents));
|
||||
QVERIFY(bus.registerService("io.nymea.RfidProvider"));
|
||||
RfidProviderClient client(nullptr, bus);
|
||||
int callbacks = 0;
|
||||
bool result = true;
|
||||
client.probe([&](bool found) { ++callbacks; result = found; });
|
||||
QTRY_COMPARE_WITH_TIMEOUT(callbacks, 1, 2500);
|
||||
QVERIFY(!result);
|
||||
QVERIFY(!client.available());
|
||||
}
|
||||
|
||||
void presenceReadinessAndRestart()
|
||||
{
|
||||
ProviderFixture provider;
|
||||
QVERIFY(bus.registerObject("/io/nymea/RfidProvider", &provider, QDBusConnection::ExportAllContents));
|
||||
QVERIFY(bus.registerService("io.nymea.RfidProvider"));
|
||||
RfidProviderClient client(nullptr, bus);
|
||||
int callbacks = 0;
|
||||
bool result = false;
|
||||
client.probe([&](bool found) { ++callbacks; result = found; });
|
||||
QTRY_COMPARE_WITH_TIMEOUT(callbacks, 1, 2500);
|
||||
QVERIFY(result);
|
||||
QVERIFY(client.available());
|
||||
QVERIFY(!client.ready());
|
||||
provider.isReady = true;
|
||||
auto update = QDBusMessage::createSignal("/io/nymea/RfidProvider", "org.freedesktop.DBus.Properties", "PropertiesChanged");
|
||||
update << QString("io.nymea.RfidProvider1") << QVariantMap{{"Ready", true}} << QStringList{};
|
||||
QVERIFY(bus.send(update));
|
||||
QTRY_VERIFY(client.ready());
|
||||
QSignalSpy detections(&client, &RfidProviderClient::tagDetected);
|
||||
const QVariantMap metadata{{"technology", "ISO14443A"}};
|
||||
emit provider.tagDetected("0001ABFF", metadata);
|
||||
QTRY_COMPARE(detections.count(), 1);
|
||||
QCOMPARE(detections.first().at(0).toString(), QString("0001ABFF"));
|
||||
QCOMPARE(detections.first().at(1).toMap(), metadata);
|
||||
QVERIFY(bus.unregisterService("io.nymea.RfidProvider"));
|
||||
QTRY_VERIFY(!client.available());
|
||||
QVERIFY(!client.ready());
|
||||
QVERIFY(bus.registerService("io.nymea.RfidProvider"));
|
||||
QTRY_VERIFY_WITH_TIMEOUT(client.available(), 2500);
|
||||
QTRY_VERIFY(client.ready());
|
||||
// A reconnect must neither replay a detection nor multiply subscriptions.
|
||||
QCOMPARE(detections.count(), 1);
|
||||
emit provider.tagDetected("0001ABFF", metadata);
|
||||
QTRY_COMPARE(detections.count(), 2);
|
||||
QTest::qWait(50);
|
||||
QCOMPARE(detections.count(), 2);
|
||||
}
|
||||
|
||||
void eligibility_data()
|
||||
{
|
||||
QTest::addColumn<QString>("url");
|
||||
QTest::addColumn<int>("evses");
|
||||
QTest::addColumn<int>("connectors");
|
||||
QTest::addColumn<bool>("ac");
|
||||
QTest::addColumn<bool>("dc");
|
||||
QTest::addColumn<bool>("eligible");
|
||||
QTest::newRow("localhost-ac") << "ws://localhost:8849" << 1 << 1 << true << false << true;
|
||||
QTest::newRow("ipv4-ac") << "ws://127.0.0.1:8849" << 1 << 1 << true << false << true;
|
||||
QTest::newRow("ipv6-ac") << "ws://[::1]:8849" << 1 << 1 << true << false << true;
|
||||
QTest::newRow("remote-ac") << "ws://192.168.1.2:8849" << 1 << 1 << true << false << false;
|
||||
QTest::newRow("deceptive-host") << "ws://localhost.example:8849" << 1 << 1 << true << false << false;
|
||||
QTest::newRow("multi-evse") << "ws://localhost:8849" << 2 << 1 << true << false << false;
|
||||
QTest::newRow("multi-connector") << "ws://localhost:8849" << 1 << 2 << true << false << false;
|
||||
QTest::newRow("no-connector") << "ws://localhost:8849" << 1 << 0 << true << false << false;
|
||||
QTest::newRow("no-evse") << "ws://localhost:8849" << 0 << 1 << true << false << false;
|
||||
QTest::newRow("dc") << "ws://localhost:8849" << 1 << 1 << false << true << false;
|
||||
QTest::newRow("mixed-ac-dc") << "ws://localhost:8849" << 1 << 1 << true << true << false;
|
||||
QTest::newRow("unknown") << "ws://localhost:8849" << 1 << 1 << false << false << false;
|
||||
}
|
||||
|
||||
void eligibility()
|
||||
{
|
||||
QFETCH(QString, url);
|
||||
QFETCH(int, evses);
|
||||
QFETCH(int, connectors);
|
||||
QFETCH(bool, ac);
|
||||
QFETCH(bool, dc);
|
||||
QFETCH(bool, eligible);
|
||||
QList<EverestJsonRpcClient::EVSEInfo> topology;
|
||||
for (int i = 0; i < evses; ++i) {
|
||||
EverestJsonRpcClient::EVSEInfo evse;
|
||||
evse.index = i;
|
||||
for (int j = 0; j < connectors; ++j) {
|
||||
EverestJsonRpcClient::ConnectorInfo connector;
|
||||
connector.connectorId = j;
|
||||
evse.availableConnectors.append(connector);
|
||||
}
|
||||
if (ac) evse.supportedEnergyTransferModes << EverestJsonRpcClient::EnergyTransferModeAC_three_phase_core;
|
||||
if (dc) evse.supportedEnergyTransferModes << EverestJsonRpcClient::EnergyTransferModeDC_core;
|
||||
topology << evse;
|
||||
}
|
||||
QCOMPARE(everestRfidEligible(QUrl(url), topology), eligible);
|
||||
}
|
||||
};
|
||||
QTEST_GUILESS_MAIN(TestRfidProviderClient)
|
||||
#include "tst_rfidproviderclient.moc"
|
||||
Loading…
x
Reference in New Issue
Block a user