From 75465f9226f0eef7d9ddd93fd9eb1e02d3d7debe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20St=C3=BCrz?= Date: Thu, 11 Dec 2025 15:18:16 +0100 Subject: [PATCH] Add proper car filtering --- dashboard/app.js | 126 ++++++++++++++++++++++++++++++++++------ dashboard/index.html | 8 +-- plugin/evdashengine.cpp | 114 ++++++++++++++++++++++++++++++++---- plugin/evdashengine.h | 5 ++ 4 files changed, 221 insertions(+), 32 deletions(-) diff --git a/dashboard/app.js b/dashboard/app.js index ebeecff..c2e95e3 100644 --- a/dashboard/app.js +++ b/dashboard/app.js @@ -18,7 +18,7 @@ class DashboardApp { chargerEmptyRow: document.getElementById('chargerEmptyRow'), fetchSessionsButton: document.getElementById('fetchSessionsButton'), downloadSessionsButton: document.getElementById('downloadSessionsButton'), - chargerFilter: document.getElementById('chargerFilter'), + carFilter: document.getElementById('carFilter'), chargingSessionsTableBody: document.getElementById('chargingSessionsTableBody'), chargingSessionsEmptyRow: document.getElementById('chargingSessionsEmptyRow'), chargingSessionsOutput: document.getElementById('chargingSessionsOutput'), @@ -36,6 +36,7 @@ class DashboardApp { this.tokenRefreshTimer = null; this.refreshInFlight = false; this.chargers = new Map(); + this.cars = new Map(); this.sessions = []; this.activePanel = null; this.chargerColumns = [ @@ -58,7 +59,7 @@ class DashboardApp { this.initializePanelNavigation(); this.restoreSession(); this.toggleChargerEmptyState(); - this.updateChargerSelector(); + this.updateCarSelector(); } attachEventListeners() { @@ -360,7 +361,9 @@ class DashboardApp { this.tokenRefreshTimer = null; this.refreshInFlight = false; this.chargers.clear(); + this.cars.clear(); this.resetChargerTable(); + this.updateCarSelector(); this.renderChargingSessions([], 'No charging sessions fetched yet.'); try { @@ -481,6 +484,19 @@ class DashboardApp { return true; } + if (type === 'getcars') { + if (data.success) { + const payload = data && data.payload ? data.payload : {}; + const cars = Array.isArray(payload.cars) ? payload.cars : []; + this.processCarList(cars); + } else if (data.error === 'unauthenticated') { + this.onAuthenticationFailed('unauthenticated'); + } else { + console.warn('GetCars request failed', data.error || 'unknownError'); + } + return true; + } + if (type === 'getchargingsessions') { if (data.success) { const payload = data && data.payload ? data.payload : {}; @@ -515,6 +531,11 @@ class DashboardApp { return true; } + if (Array.isArray(payload.cars)) { + this.processCarList(payload.cars); + return true; + } + if (Array.isArray(payload.sessions)) { this.renderChargingSessions(payload.sessions); return true; @@ -525,6 +546,11 @@ class DashboardApp { return true; } + if (payload.car) { + this.upsertCar(payload.car); + return true; + } + return false; } @@ -541,6 +567,13 @@ class DashboardApp { case 'chargerremoved': this.removeCharger(payload); return true; + case 'caradded': + case 'carchanged': + this.upsertCar(payload); + return true; + case 'carremoved': + this.removeCar(payload); + return true; case 'chargingsessionsupdated': if (payload && Array.isArray(payload.sessions)) this.renderChargingSessions(payload.sessions); @@ -553,6 +586,7 @@ class DashboardApp { onAuthenticationSucceeded() { this.updateConnectionStatus('Connected', 'connected'); this.updateSessionUser(); + this.sendGetCars(); this.sendGetChargers(); } @@ -621,15 +655,19 @@ class DashboardApp { return this.sendAction('ping', { timestamp: new Date().toISOString() }); } + sendGetCars() { + return this.sendAction('GetCars', { }); + } + sendGetChargers() { return this.sendAction('GetChargers', { }); } fetchChargingSessions() { const payload = {}; - const chargerId = this.elements.chargerFilter ? this.elements.chargerFilter.value : ''; - if (chargerId) - payload.chargerId = chargerId; + const carId = this.elements.carFilter ? this.elements.carFilter.value : ''; + if (carId) + payload.carId = carId; const requestId = this.sendAction('GetChargingSessions', payload); if (!requestId) @@ -657,8 +695,6 @@ class DashboardApp { if (!seen.has(existingId)) this.removeCharger(existingId); } - - this.updateChargerSelector(); } upsertCharger(charger) { @@ -672,7 +708,6 @@ class DashboardApp { merged.thingId = key; this.chargers.set(key, merged); this.syncChargerRow(merged, !hasExisting); - this.updateChargerSelector(); } syncChargerRow(charger, forceCreate = false) { @@ -747,7 +782,6 @@ class DashboardApp { row.parentElement.removeChild(row); this.toggleChargerEmptyState(); - this.updateChargerSelector(); } resetChargerTable() { @@ -761,7 +795,6 @@ class DashboardApp { }); this.toggleChargerEmptyState(); - this.updateChargerSelector(); } findChargerRow(chargerId) { @@ -798,8 +831,67 @@ class DashboardApp { this.elements.chargerEmptyRow.classList.toggle('hidden', hasChargers); } - updateChargerSelector() { - const select = this.elements.chargerFilter; + processCarList(cars = []) { + if (!Array.isArray(cars)) { + console.warn('Expected cars array in payload.'); + return; + } + + const seen = new Set(); + cars.forEach(car => { + const key = this.getCarKey(car); + if (!key) + return; + seen.add(key); + this.upsertCar(car); + }); + + for (const existingId of Array.from(this.cars.keys())) { + if (!seen.has(existingId)) + this.removeCar(existingId); + } + } + + upsertCar(car) { + const key = this.getCarKey(car); + if (!key) + return; + + const hasExisting = this.cars.has(key); + const previous = hasExisting ? this.cars.get(key) : {}; + const merged = { ...previous, ...car }; + merged.thingId = key; + this.cars.set(key, merged); + this.updateCarSelector(); + } + + removeCar(identifier) { + const key = this.getCarKey(identifier); + if (!key) + return; + + this.cars.delete(key); + this.updateCarSelector(); + } + + getCarKey(source) { + if (!source) + return null; + + if (typeof source === 'string') + return source; + + if (source.thingId) + return source.thingId; + + if (source.id) + return source.id; + + return null; + } + + updateCarSelector() { + const select = this.elements.carFilter; if (!select) return; @@ -809,16 +901,16 @@ class DashboardApp { const defaultOption = document.createElement('option'); defaultOption.value = ''; - defaultOption.textContent = 'All chargers'; + defaultOption.textContent = 'All cars'; select.appendChild(defaultOption); - const chargers = Array.from(this.chargers.values()) + const cars = Array.from(this.cars.values()) .sort((a, b) => (a.name || '').localeCompare(b.name || '', undefined, { sensitivity: 'base' })); - chargers.forEach(charger => { + cars.forEach(car => { const option = document.createElement('option'); - option.value = this.getChargerKey(charger) || ''; - option.textContent = charger.name || option.value; + option.value = this.getCarKey(car) || ''; + option.textContent = car.name || option.value; select.appendChild(option); }); diff --git a/dashboard/index.html b/dashboard/index.html index 627b6bb..ffc0448 100644 --- a/dashboard/index.html +++ b/dashboard/index.html @@ -687,15 +687,15 @@

Charging sessions

- - +
-

Select a charger to filter by its assigned car and fetch sessions from nymea.

+

Select a car to filter charging sessions fetched from nymea.

diff --git a/plugin/evdashengine.cpp b/plugin/evdashengine.cpp index c965f15..c90e4ca 100644 --- a/plugin/evdashengine.cpp +++ b/plugin/evdashengine.cpp @@ -56,12 +56,17 @@ EvDashEngine::EvDashEngine(ThingManager *thingManager, EvDashWebServerResource * m_thingManager{thingManager}, m_webServerResource{webServerResource} { - Things evChargers = m_thingManager->configuredThings().filterByInterface("evcharger"); + Things configuredThings = m_thingManager->configuredThings(); + foreach (Thing *thing, configuredThings) { + if (isChargerThing(thing)) { + m_chargers.append(thing); + monitorChargerThing(thing); + } - // Init charger list - foreach (Thing *chargerThing, evChargers) { - m_chargers.append(chargerThing); - monitorChargerThing(chargerThing); + if (isCarThing(thing)) { + m_cars.append(thing); + monitorCarThing(thing); + } } connect(m_thingManager, &ThingManager::thingAdded, this, &EvDashEngine::onThingAdded); @@ -191,11 +196,17 @@ bool EvDashEngine::setEnabled(bool enabled) void EvDashEngine::onThingAdded(Thing *thing) { - if (thing->thingClass().interfaces().contains("evcharger")) { + if (isChargerThing(thing)) { m_chargers.append(thing); monitorChargerThing(thing); sendNotification("ChargerAdded", packCharger(thing)); } + + if (isCarThing(thing)) { + m_cars.append(thing); + monitorCarThing(thing); + sendNotification("CarAdded", packCar(thing)); + } } void EvDashEngine::onThingRemoved(const ThingId &thingId) @@ -205,13 +216,27 @@ void EvDashEngine::onThingRemoved(const ThingId &thingId) qCDebug(dcEvDashExperience()) << "Charger has been removed."; m_chargers.removeAll(thing); sendNotification("ChargerRemoved", packCharger(thing)); + break; + } + } + + foreach (Thing *thing, m_cars) { + if (thing->id() == thingId) { + qCDebug(dcEvDashExperience()) << "Car has been removed."; + m_cars.removeAll(thing); + sendNotification("CarRemoved", packCar(thing)); + break; } } } void EvDashEngine::onThingChanged(Thing *thing) { - sendNotification("ChargerChanged", packCharger(thing)); + if (isChargerThing(thing)) + sendNotification("ChargerChanged", packCharger(thing)); + + if (isCarThing(thing)) + sendNotification("CarChanged", packCar(thing)); } void EvDashEngine::monitorChargerThing(Thing *thing) @@ -227,6 +252,19 @@ void EvDashEngine::monitorChargerThing(Thing *thing) }); } +void EvDashEngine::monitorCarThing(Thing *thing) +{ + connect(thing, &Thing::stateValueChanged, this, [this, thing](const StateTypeId &stateTypeId, const QVariant &value, const QVariant &minValue, const QVariant &maxValue, const QVariantList &possibleValues){ + Q_UNUSED(stateTypeId) + Q_UNUSED(value) + Q_UNUSED(minValue) + Q_UNUSED(maxValue) + Q_UNUSED(possibleValues) + + onThingChanged(thing); + }); +} + bool EvDashEngine::startWebSocketServer(quint16 port) { if (m_webSocketServer->isListening()) { @@ -356,21 +394,45 @@ QJsonObject EvDashEngine::handleApiRequest(QWebSocket *socket, const QJsonObject QJsonObject payload; QJsonArray chargerList; - for (Thing *charger : m_thingManager->configuredThings().filterByInterface("evcharger")) { - chargerList.append(packCharger(charger)); + foreach (Thing *thing, m_thingManager->configuredThings()) { + if (isChargerThing(thing)) + chargerList.append(packCharger(thing)); } payload.insert(QStringLiteral("chargers"), chargerList); return createSuccessResponse(requestId, payload); } + if (action.compare(QStringLiteral("GetCars"), Qt::CaseInsensitive) == 0) { + QJsonObject payload; + QJsonArray carList; + foreach (Thing *thing, m_thingManager->configuredThings()) { + if (isCarThing(thing)) + carList.append(packCar(thing)); + } + + payload.insert(QStringLiteral("cars"), carList); + return createSuccessResponse(requestId, payload); + } + if (action.compare(QStringLiteral("GetChargingSessions"), Qt::CaseInsensitive) == 0) { if (!m_chargingSessionsClient) return createErrorResponse(requestId, QStringLiteral("chargingSessionsUnavailable")); const QJsonObject payload = request.value(QStringLiteral("payload")).toObject(); - const QString chargerId = payload.value(QStringLiteral("chargerId")).toString(); - const QStringList carThingIds = carThingIdsForCharger(chargerId); + QStringList carThingIds; + + const QString carId = payload.value(QStringLiteral("carId")).toString(); + if (!carId.isEmpty()) { + const QUuid carUuid = QUuid::fromString(carId); + if (carUuid.isNull()) + return createErrorResponse(requestId, QStringLiteral("invalidCarId")); + carThingIds.append(carUuid.toString(QUuid::WithoutBraces)); + } else { + const QString chargerId = payload.value(QStringLiteral("chargerId")).toString(); + if (!chargerId.isEmpty()) + carThingIds = carThingIdsForCharger(chargerId); + } m_pendingChargingSessionsRequests.insert(requestId, QPointer(socket)); m_chargingSessionsClient->getSessions(carThingIds); @@ -480,6 +542,36 @@ QJsonObject EvDashEngine::packCharger(Thing *charger) const return chargerObject; } +QJsonObject EvDashEngine::packCar(Thing *car) const +{ + QJsonObject carObject; + if (!car) + return carObject; + + carObject.insert("id", car->id().toString(QUuid::WithoutBraces)); + carObject.insert("name", car->name()); + + return carObject; +} + +bool EvDashEngine::isChargerThing(Thing *thing) const +{ + if (!thing) + return false; + + const QStringList interfaces = thing->thingClass().interfaces(); + return interfaces.contains(QStringLiteral("evcharger")); +} + +bool EvDashEngine::isCarThing(Thing *thing) const +{ + if (!thing) + return false; + + const QStringList interfaces = thing->thingClass().interfaces(); + return interfaces.contains(QStringLiteral("electricvehicle")); +} + QStringList EvDashEngine::carThingIdsForCharger(const QString &chargerId) const { QStringList carThingIds; diff --git a/plugin/evdashengine.h b/plugin/evdashengine.h index 89b8585..54b1278 100644 --- a/plugin/evdashengine.h +++ b/plugin/evdashengine.h @@ -92,10 +92,14 @@ private: QList m_chargers; void monitorChargerThing(Thing *thing); + QList m_cars; + void monitorCarThing(Thing *thing); // Pending requests waiting for charging sessions data to return QHash> m_pendingChargingSessionsRequests; QStringList carThingIdsForCharger(const QString &chargerId) const; + bool isChargerThing(Thing *thing) const; + bool isCarThing(Thing *thing) const; // Websocket server bool startWebSocketServer(quint16 port = 0); @@ -111,6 +115,7 @@ private: QJsonObject createErrorResponse(const QString &requestId, const QString &errorMessage) const; QJsonObject packCharger(Thing *charger) const; + QJsonObject packCar(Thing *car) const; void onSessionsReceived(const QList &sessions); void onSessionsError(const QString &errorMessage);