Add proper car filtering
This commit is contained in:
parent
cd1c30223f
commit
75465f9226
126
dashboard/app.js
126
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);
|
||||
});
|
||||
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user