Add proper car filtering

This commit is contained in:
Simon Stürz 2025-12-11 15:18:16 +01:00
parent cd1c30223f
commit 75465f9226
4 changed files with 221 additions and 32 deletions

View File

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

View File

@ -687,15 +687,15 @@
<h2 id="chargingSessionsTitle">Charging sessions</h2>
</div>
<div class="action-row">
<label class="sr-only" for="chargerFilter">Charger</label>
<select id="chargerFilter" name="chargerFilter">
<option value="">All chargers</option>
<label class="sr-only" for="carFilter">Car</label>
<select id="carFilter" name="carFilter">
<option value="">All cars</option>
</select>
<button type="button" id="fetchSessionsButton" class="primary">Fetch sessions</button>
<button type="button" id="downloadSessionsButton">Download CSV</button>
</div>
</div>
<p class="helper-text">Select a charger to filter by its assigned car and fetch sessions from nymea.</p>
<p class="helper-text">Select a car to filter charging sessions fetched from nymea.</p>
<div class="table-wrapper">
<table class="data-table sessions-table">
<thead>

View File

@ -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<QWebSocket>(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;

View File

@ -92,10 +92,14 @@ private:
QList<Thing *> m_chargers;
void monitorChargerThing(Thing *thing);
QList<Thing *> m_cars;
void monitorCarThing(Thing *thing);
// Pending requests waiting for charging sessions data to return
QHash<QString, QPointer<QWebSocket>> 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<QVariantMap> &sessions);
void onSessionsError(const QString &errorMessage);