EVerest: Add RFID thing class and functionality for the nymea-rfid-provider

This commit is contained in:
Simon Stürz 2026-10-13 16:38:21 +02:00
parent 4820e3a56f
commit 073ff88f32
11 changed files with 970 additions and 93 deletions

View File

@ -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

View File

@ -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 \

View File

@ -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());
}

View File

@ -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

View File

@ -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 @@
]
}

View File

@ -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);

View 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

View 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();
}
}

View 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

View 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>)

View 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"