mirror of
https://github.com/nymea/nymea-plugins.git
synced 2026-10-23 14:43:44 +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.
|
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
|
## Using the MQTT based API module
|
||||||
|
|
||||||
|
|||||||
@ -1,9 +1,10 @@
|
|||||||
include(../plugins.pri)
|
include(../plugins.pri)
|
||||||
|
|
||||||
QT += network websockets
|
QT += network websockets dbus
|
||||||
PKGCONFIG += nymea-mqtt
|
PKGCONFIG += nymea-mqtt
|
||||||
|
|
||||||
SOURCES += \
|
SOURCES += \
|
||||||
|
rfid/rfidproviderclient.cpp \
|
||||||
jsonrpc/everestconnection.cpp \
|
jsonrpc/everestconnection.cpp \
|
||||||
jsonrpc/everestevse.cpp \
|
jsonrpc/everestevse.cpp \
|
||||||
jsonrpc/everestjsonrpcclient.cpp \
|
jsonrpc/everestjsonrpcclient.cpp \
|
||||||
@ -16,6 +17,8 @@ SOURCES += \
|
|||||||
integrationplugineverest.cpp
|
integrationplugineverest.cpp
|
||||||
|
|
||||||
HEADERS += \
|
HEADERS += \
|
||||||
|
rfid/rfidproviderclient.h \
|
||||||
|
rfid/everestrfidutils.h \
|
||||||
jsonrpc/everestconnection.h \
|
jsonrpc/everestconnection.h \
|
||||||
jsonrpc/everestevse.h \
|
jsonrpc/everestevse.h \
|
||||||
jsonrpc/everestjsonrpcclient.h \
|
jsonrpc/everestjsonrpcclient.h \
|
||||||
|
|||||||
@ -28,9 +28,12 @@
|
|||||||
#include "mqtt/everestmqttdiscovery.h"
|
#include "mqtt/everestmqttdiscovery.h"
|
||||||
#include "jsonrpc/everestevse.h"
|
#include "jsonrpc/everestevse.h"
|
||||||
#include "jsonrpc/everestjsonrpcdiscovery.h"
|
#include "jsonrpc/everestjsonrpcdiscovery.h"
|
||||||
|
#include "rfid/everestrfidutils.h"
|
||||||
|
|
||||||
#include <QFileInfo>
|
#include <QFileInfo>
|
||||||
|
#include <QPointer>
|
||||||
|
|
||||||
|
#include <hardwaremanager.h>
|
||||||
#include <nymeasettings.h>
|
#include <nymeasettings.h>
|
||||||
#include <network/networkdevicediscovery.h>
|
#include <network/networkdevicediscovery.h>
|
||||||
|
|
||||||
@ -41,6 +44,16 @@ IntegrationPluginEverest::IntegrationPluginEverest()
|
|||||||
|
|
||||||
void IntegrationPluginEverest::init()
|
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");
|
QFileInfo enableMqttFileInfo(NymeaSettings::settingsPath() + "/everest-mqtt");
|
||||||
m_useMqtt = enableMqttFileInfo.exists();
|
m_useMqtt = enableMqttFileInfo.exists();
|
||||||
if (m_useMqtt) {
|
if (m_useMqtt) {
|
||||||
@ -399,74 +412,98 @@ void IntegrationPluginEverest::setupThing(ThingSetupInfo *info)
|
|||||||
m_everstConnections.insert(thing, connection);
|
m_everstConnections.insert(thing, connection);
|
||||||
|
|
||||||
connect(connection, &EverestConnection::availableChanged, thing, [this, thing, connection](bool available){
|
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);
|
thing->setStateValue(everestConnectionConnectedStateTypeId, available);
|
||||||
// Update connected state of child things
|
// Update connected state of child things
|
||||||
foreach (Thing *child, myThings().filterByParentId(thing->id())) {
|
foreach (Thing *child, myThings().filterByParentId(thing->id())) {
|
||||||
child->setStateValue("connected", available);
|
child->setStateValue("connected", available);
|
||||||
}
|
}
|
||||||
|
updateRfidAvailability();
|
||||||
|
|
||||||
if (available) {
|
if (available) {
|
||||||
thing->setStateValue(everestConnectionApiVersionStateTypeId, connection->client()->apiVersion());
|
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;
|
// Sync things with availabe EvseInfos from this connection
|
||||||
ThingDescriptors descriptors;
|
|
||||||
qCDebug(dcEverest()) << "The client is now available, synching things...";
|
|
||||||
|
|
||||||
// First check if we have a thing for each index, if not, create the thing for it
|
QList<int> indexList;
|
||||||
foreach (const EverestJsonRpcClient::EVSEInfo &evseInfo, connection->client()->evseInfos()) {
|
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.
|
// First check if we have a thing for each index, if not, create the thing for it
|
||||||
indexList.append(evseInfo.index);
|
foreach (const EverestJsonRpcClient::EVSEInfo &evseInfo, connection->client()->evseInfos()) {
|
||||||
|
|
||||||
// Check if we already have a child device for this index
|
// Collect all EVSE index in onder to check later if we have a thing for a dissapeard index.
|
||||||
bool alreadyAdded = false;
|
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())) {
|
foreach (Thing *childThing, myThings().filterByParentId(thing->id())) {
|
||||||
if (childThing->paramValue("index").toInt() == evseInfo.index) {
|
int thingIndex = childThing->paramValue("index").toInt();
|
||||||
qCDebug(dcEverest()) << "-> Found already added charger with index" << evseInfo.index;
|
if (!indexList.contains(thingIndex)) {
|
||||||
alreadyAdded = true;
|
qCDebug(dcEverest()) << "Removing thing" << childThing->name() << "because the index" << thingIndex << "does not exist any more on this connection.";
|
||||||
break;
|
emit autoThingDisappeared(childThing->id());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!alreadyAdded) {
|
if (!descriptors.isEmpty()) {
|
||||||
if (evseInfo.supportedEnergyTransferModes.contains(EverestJsonRpcClient::EnergyTransferModeDC)
|
emit autoThingsAppeared(descriptors);
|
||||||
|| 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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
|
|
||||||
// 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();
|
connection->start();
|
||||||
return;
|
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());
|
Thing *parentThing = myThings().findById(thing->parentId());
|
||||||
EverestConnection *connection = m_everstConnections.value(parentThing);
|
EverestConnection *connection = m_everstConnections.value(parentThing);
|
||||||
@ -486,12 +523,30 @@ void IntegrationPluginEverest::setupThing(ThingSetupInfo *info)
|
|||||||
info->finish(Thing::ThingErrorNoError);
|
info->finish(Thing::ThingErrorNoError);
|
||||||
|
|
||||||
connection->addThing(thing);
|
connection->addThing(thing);
|
||||||
|
QTimer::singleShot(0, this, &IntegrationPluginEverest::updateRfidAvailability);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void IntegrationPluginEverest::executeAction(ThingActionInfo *info)
|
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()
|
qCDebug(dcEverest()) << "Executing action for thing" << info->thing()
|
||||||
<< info->action().actionTypeId().toString() << info->action().params();
|
<< info->action().actionTypeId().toString() << info->action().params();
|
||||||
|
|
||||||
@ -543,7 +598,7 @@ void IntegrationPluginEverest::executeAction(ThingActionInfo *info)
|
|||||||
}
|
}
|
||||||
|
|
||||||
return;
|
return;
|
||||||
} else if (info->thing()->thingClassId() == everestChargerAcThingClassId) {
|
} else if (info->thing()->thingClassId() == everestChargerAcThingClassId || info->thing()->thingClassId() == everestChargerAcRfidThingClassId) {
|
||||||
Thing *thing = info->thing();
|
Thing *thing = info->thing();
|
||||||
Thing *parentThing = myThings().findById(thing->parentId());
|
Thing *parentThing = myThings().findById(thing->parentId());
|
||||||
EverestConnection *connection = m_everstConnections.value(parentThing);
|
EverestConnection *connection = m_everstConnections.value(parentThing);
|
||||||
@ -552,7 +607,7 @@ void IntegrationPluginEverest::executeAction(ThingActionInfo *info)
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!thing->stateValue(everestChargerAcConnectedStateTypeId).toBool()) {
|
if (!thing->stateValue(QStringLiteral("connected")).toBool()) {
|
||||||
info->finish(Thing::ThingErrorHardwareNotAvailable);
|
info->finish(Thing::ThingErrorHardwareNotAvailable);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -563,8 +618,8 @@ void IntegrationPluginEverest::executeAction(ThingActionInfo *info)
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (info->action().actionTypeId() == everestChargerAcPowerActionTypeId) {
|
if (info->action().actionTypeId() == everestChargerAcPowerActionTypeId || info->action().actionTypeId() == everestChargerAcRfidPowerActionTypeId) {
|
||||||
bool power = info->action().paramValue(everestChargerAcPowerActionPowerParamTypeId).toBool();
|
bool power = info->action().paramValue(thing->thingClassId() == everestChargerAcRfidThingClassId ? everestChargerAcRfidPowerActionPowerParamTypeId : everestChargerAcPowerActionPowerParamTypeId).toBool();
|
||||||
qCDebug(dcEverest()) << "Execute power action" << power;
|
qCDebug(dcEverest()) << "Execute power action" << power;
|
||||||
EverestJsonRpcReply *reply = evse->setChargingAllowed(power) ;
|
EverestJsonRpcReply *reply = evse->setChargingAllowed(power) ;
|
||||||
connect(reply, &EverestJsonRpcReply::finished, reply, &EverestJsonRpcReply::deleteLater);
|
connect(reply, &EverestJsonRpcReply::finished, reply, &EverestJsonRpcReply::deleteLater);
|
||||||
@ -583,11 +638,11 @@ void IntegrationPluginEverest::executeAction(ThingActionInfo *info)
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
info->thing()->setStateValue(everestChargerAcCurrentPowerStateTypeId, power);
|
info->thing()->setStateValue(QStringLiteral("power"), power);
|
||||||
info->finish(Thing::ThingErrorNoError);
|
info->finish(Thing::ThingErrorNoError);
|
||||||
});
|
});
|
||||||
} else if (info->action().actionTypeId() == everestChargerAcMaxChargingCurrentActionTypeId) {
|
} else if (info->action().actionTypeId() == everestChargerAcMaxChargingCurrentActionTypeId || info->action().actionTypeId() == everestChargerAcRfidMaxChargingCurrentActionTypeId) {
|
||||||
double current = info->action().paramValue(everestChargerAcMaxChargingCurrentActionMaxChargingCurrentParamTypeId).toDouble();
|
double current = info->action().paramValue(thing->thingClassId() == everestChargerAcRfidThingClassId ? everestChargerAcRfidMaxChargingCurrentActionMaxChargingCurrentParamTypeId : everestChargerAcMaxChargingCurrentActionMaxChargingCurrentParamTypeId).toDouble();
|
||||||
qCDebug(dcEverest()) << "Execute action set max charging current" << current << "[A]";
|
qCDebug(dcEverest()) << "Execute action set max charging current" << current << "[A]";
|
||||||
EverestJsonRpcReply *reply = evse->setACChargingCurrent(current) ;
|
EverestJsonRpcReply *reply = evse->setACChargingCurrent(current) ;
|
||||||
connect(reply, &EverestJsonRpcReply::finished, reply, &EverestJsonRpcReply::deleteLater);
|
connect(reply, &EverestJsonRpcReply::finished, reply, &EverestJsonRpcReply::deleteLater);
|
||||||
@ -606,11 +661,11 @@ void IntegrationPluginEverest::executeAction(ThingActionInfo *info)
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
info->thing()->setStateValue(everestChargerAcMaxChargingCurrentStateTypeId, current);
|
info->thing()->setStateValue(QStringLiteral("maxChargingCurrent"), current);
|
||||||
info->finish(Thing::ThingErrorNoError);
|
info->finish(Thing::ThingErrorNoError);
|
||||||
});
|
});
|
||||||
} else if (info->action().actionTypeId() == everestChargerAcDesiredPhaseCountActionTypeId) {
|
} else if (info->action().actionTypeId() == everestChargerAcDesiredPhaseCountActionTypeId || info->action().actionTypeId() == everestChargerAcRfidDesiredPhaseCountActionTypeId) {
|
||||||
int phaseCount = info->action().paramValue(everestChargerAcDesiredPhaseCountActionDesiredPhaseCountParamTypeId).toInt();
|
int phaseCount = info->action().paramValue(thing->thingClassId() == everestChargerAcRfidThingClassId ? everestChargerAcRfidDesiredPhaseCountActionDesiredPhaseCountParamTypeId : everestChargerAcDesiredPhaseCountActionDesiredPhaseCountParamTypeId).toInt();
|
||||||
qCDebug(dcEverest()) << "Execute action set phase count" << phaseCount;
|
qCDebug(dcEverest()) << "Execute action set phase count" << phaseCount;
|
||||||
EverestJsonRpcReply *reply = evse->setACChargingPhaseCount(phaseCount);
|
EverestJsonRpcReply *reply = evse->setACChargingPhaseCount(phaseCount);
|
||||||
connect(reply, &EverestJsonRpcReply::finished, reply, &EverestJsonRpcReply::deleteLater);
|
connect(reply, &EverestJsonRpcReply::finished, reply, &EverestJsonRpcReply::deleteLater);
|
||||||
@ -628,7 +683,7 @@ void IntegrationPluginEverest::executeAction(ThingActionInfo *info)
|
|||||||
info->finish(Thing::ThingErrorHardwareFailure);
|
info->finish(Thing::ThingErrorHardwareFailure);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
info->thing()->setStateValue(everestChargerAcDesiredPhaseCountStateTypeId, phaseCount);
|
info->thing()->setStateValue(QStringLiteral("desiredPhaseCount"), phaseCount);
|
||||||
info->finish(Thing::ThingErrorNoError);
|
info->finish(Thing::ThingErrorNoError);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -731,7 +786,7 @@ void IntegrationPluginEverest::thingRemoved(Thing *thing)
|
|||||||
}
|
}
|
||||||
} else if (thing->thingClassId() == everestConnectionThingClassId) {
|
} else if (thing->thingClassId() == everestConnectionThingClassId) {
|
||||||
m_everstConnections.take(thing)->deleteLater();
|
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());
|
Thing *parentThing = myThings().findById(thing->parentId());
|
||||||
EverestConnection *connection = m_everstConnections.value(parentThing);
|
EverestConnection *connection = m_everstConnections.value(parentThing);
|
||||||
if (!connection)
|
if (!connection)
|
||||||
@ -739,4 +794,37 @@ void IntegrationPluginEverest::thingRemoved(Thing *thing)
|
|||||||
|
|
||||||
connection->removeThing(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 "mqtt/everestmqttclient.h"
|
||||||
#include "jsonrpc/everestconnection.h"
|
#include "jsonrpc/everestconnection.h"
|
||||||
|
#include "rfid/rfidproviderclient.h"
|
||||||
|
|
||||||
#include <mqttclient.h>
|
#include <mqttclient.h>
|
||||||
|
|
||||||
@ -60,6 +61,9 @@ private:
|
|||||||
QHash<Thing *, EverestMqttClient *> m_thingClients;
|
QHash<Thing *, EverestMqttClient *> m_thingClients;
|
||||||
|
|
||||||
QHash<Thing *, EverestConnection *> m_everstConnections;
|
QHash<Thing *, EverestConnection *> m_everstConnections;
|
||||||
|
RfidProviderClient *m_rfidProvider = nullptr;
|
||||||
|
Thing *rfidTarget() const;
|
||||||
|
void updateRfidAvailability();
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif // INTEGRATIONPLUGINEVEREST_H
|
#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",
|
"name": "everestChargerAc",
|
||||||
"displayName": "EVerest AC Charger",
|
"displayName": "EVerest AC Charger",
|
||||||
@ -745,4 +1052,3 @@
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -211,18 +211,18 @@ void EverestEvse::initialize()
|
|||||||
|
|
||||||
void EverestEvse::processEvseStatus()
|
void EverestEvse::processEvseStatus()
|
||||||
{
|
{
|
||||||
if (m_thing->thingClassId() == everestChargerAcThingClassId) {
|
if ((m_thing->thingClassId() == everestChargerAcThingClassId || m_thing->thingClassId() == everestChargerAcRfidThingClassId)) {
|
||||||
m_thing->setStateValue(everestChargerAcPowerStateTypeId, m_evseStatus.chargingAllowed);
|
m_thing->setStateValue(QStringLiteral("power"), m_evseStatus.chargingAllowed);
|
||||||
m_thing->setStateValue(everestChargerAcSessionEnergyStateTypeId, m_evseStatus.chargedEnergyWh / 1000.0);
|
m_thing->setStateValue(QStringLiteral("sessionEnergy"), m_evseStatus.chargedEnergyWh / 1000.0);
|
||||||
m_thing->setStateValue(everestChargerAcStateStateTypeId, m_evseStatus.evseStateString);
|
m_thing->setStateValue(QStringLiteral("state"), m_evseStatus.evseStateString);
|
||||||
m_thing->setStateValue(everestChargerAcChargingStateTypeId, m_evseStatus.evseState == EverestJsonRpcClient::EvseStateCharging);
|
m_thing->setStateValue(QStringLiteral("charging"), m_evseStatus.evseState == EverestJsonRpcClient::EvseStateCharging);
|
||||||
m_thing->setStateValue(everestChargerAcPluggedInStateTypeId, m_evseStatus.evseState != EverestJsonRpcClient::EvseStateUnplugged);
|
m_thing->setStateValue(QStringLiteral("pluggedIn"), m_evseStatus.evseState != EverestJsonRpcClient::EvseStateUnplugged);
|
||||||
|
|
||||||
m_thing->setStateValue(everestChargerAcPhaseCountStateTypeId, m_evseStatus.acChargeStatus.activePhaseCount);
|
m_thing->setStateValue(QStringLiteral("phaseCount"), m_evseStatus.acChargeStatus.activePhaseCount);
|
||||||
m_thing->setStateValue(everestChargerAcDesiredPhaseCountStateTypeId, m_evseStatus.acChargeStatus.activePhaseCount);
|
m_thing->setStateValue(QStringLiteral("desiredPhaseCount"), m_evseStatus.acChargeStatus.activePhaseCount);
|
||||||
|
|
||||||
if (m_evseStatus.acChargeParameters.maxCurrent > 0)
|
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) {
|
} else if (m_thing->thingClassId() == everestChargerDcThingClassId) {
|
||||||
|
|
||||||
@ -248,40 +248,40 @@ void EverestEvse::processEvseStatus()
|
|||||||
|
|
||||||
void EverestEvse::processHardwareCapabilities()
|
void EverestEvse::processHardwareCapabilities()
|
||||||
{
|
{
|
||||||
if (m_thing->thingClassId() == everestChargerAcThingClassId) {
|
if ((m_thing->thingClassId() == everestChargerAcThingClassId || m_thing->thingClassId() == everestChargerAcRfidThingClassId)) {
|
||||||
if (!m_hardwareCapabilities.phaseSwitchDuringCharging) {
|
if (!m_hardwareCapabilities.phaseSwitchDuringCharging) {
|
||||||
// Only option left is set the desired phase count to 3, force that value
|
// Only option left is set the desired phase count to 3, force that value
|
||||||
m_thing->setStatePossibleValues(everestChargerAcDesiredPhaseCountStateTypeId, { 3 });
|
m_thing->setStatePossibleValues(QStringLiteral("desiredPhaseCount"), { 3 });
|
||||||
m_thing->setStateValue(everestChargerAcDesiredPhaseCountStateTypeId, 3);
|
m_thing->setStateValue(QStringLiteral("desiredPhaseCount"), 3);
|
||||||
m_thing->setStateValue(everestChargerAcPhaseCountStateTypeId, 3);
|
m_thing->setStateValue(QStringLiteral("phaseCount"), 3);
|
||||||
} else {
|
} else {
|
||||||
m_thing->setStatePossibleValues(everestChargerAcDesiredPhaseCountStateTypeId, { 1, 3 });
|
m_thing->setStatePossibleValues(QStringLiteral("desiredPhaseCount"), { 1, 3 });
|
||||||
}
|
}
|
||||||
|
|
||||||
m_thing->setStateMaxValue(everestChargerAcMaxChargingCurrentStateTypeId, m_hardwareCapabilities.maxCurrentImport);
|
m_thing->setStateMaxValue(QStringLiteral("maxChargingCurrent"), m_hardwareCapabilities.maxCurrentImport);
|
||||||
m_thing->setStateMinValue(everestChargerAcMaxChargingCurrentStateTypeId, m_hardwareCapabilities.minCurrentImport == 0 ? 6 : m_hardwareCapabilities.minCurrentImport);
|
m_thing->setStateMinValue(QStringLiteral("maxChargingCurrent"), m_hardwareCapabilities.minCurrentImport == 0 ? 6 : m_hardwareCapabilities.minCurrentImport);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void EverestEvse::processMeterData()
|
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(QStringLiteral("currentPowerPhaseA"), m_meterData.powerL1);
|
||||||
m_thing->setStateValue(everestChargerAcCurrentPowerPhaseBStateTypeId, m_meterData.powerL2);
|
m_thing->setStateValue(QStringLiteral("currentPowerPhaseB"), m_meterData.powerL2);
|
||||||
m_thing->setStateValue(everestChargerAcCurrentPowerPhaseCStateTypeId, m_meterData.powerL3);
|
m_thing->setStateValue(QStringLiteral("currentPowerPhaseC"), m_meterData.powerL3);
|
||||||
|
|
||||||
m_thing->setStateValue(everestChargerAcCurrentPhaseAStateTypeId, m_meterData.currentL1);
|
m_thing->setStateValue(QStringLiteral("currentPhaseA"), m_meterData.currentL1);
|
||||||
m_thing->setStateValue(everestChargerAcCurrentPhaseBStateTypeId, m_meterData.currentL2);
|
m_thing->setStateValue(QStringLiteral("currentPhaseB"), m_meterData.currentL2);
|
||||||
m_thing->setStateValue(everestChargerAcCurrentPhaseCStateTypeId, m_meterData.currentL3);
|
m_thing->setStateValue(QStringLiteral("currentPhaseC"), m_meterData.currentL3);
|
||||||
|
|
||||||
m_thing->setStateValue(everestChargerAcVoltagePhaseAStateTypeId, m_meterData.voltageL1);
|
m_thing->setStateValue(QStringLiteral("voltagePhaseA"), m_meterData.voltageL1);
|
||||||
m_thing->setStateValue(everestChargerAcVoltagePhaseBStateTypeId, m_meterData.voltageL2);
|
m_thing->setStateValue(QStringLiteral("voltagePhaseB"), m_meterData.voltageL2);
|
||||||
m_thing->setStateValue(everestChargerAcVoltagePhaseCStateTypeId, m_meterData.voltageL3);
|
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) {
|
} else if (m_thing->thingClassId() == everestChargerDcThingClassId) {
|
||||||
|
|
||||||
m_thing->setStateValue(everestChargerDcCurrentPowerStateTypeId, m_meterData.powerTotal);
|
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