Add notifications
parent
842a4510f0
commit
cc476812fb
|
|
@ -3,5 +3,6 @@
|
|||
<qresource prefix="/">
|
||||
<file>dashboard/app.js</file>
|
||||
<file>dashboard/index.html</file>
|
||||
<file>dashboard/help.html</file>
|
||||
</qresource>
|
||||
</RCC>
|
||||
|
|
|
|||
|
|
@ -333,6 +333,8 @@ class DashboardApp {
|
|||
return;
|
||||
}
|
||||
|
||||
console.log('<--', data);
|
||||
|
||||
if (this.elements.incomingMessage)
|
||||
this.elements.incomingMessage.textContent = JSON.stringify(data, null, 2);
|
||||
|
||||
|
|
@ -380,7 +382,13 @@ class DashboardApp {
|
|||
}
|
||||
|
||||
handleUnsolicitedMessage(data) {
|
||||
if (!data || !data.payload)
|
||||
if (!data)
|
||||
return false;
|
||||
|
||||
if (data.event && this.handleNotificationEvent(data.event, data.payload))
|
||||
return true;
|
||||
|
||||
if (!data.payload)
|
||||
return false;
|
||||
|
||||
const payload = data.payload;
|
||||
|
|
@ -390,7 +398,7 @@ class DashboardApp {
|
|||
return true;
|
||||
}
|
||||
|
||||
if (payload.charger && payload.charger.id) {
|
||||
if (payload.charger) {
|
||||
this.upsertCharger(payload.charger);
|
||||
return true;
|
||||
}
|
||||
|
|
@ -398,6 +406,24 @@ class DashboardApp {
|
|||
return false;
|
||||
}
|
||||
|
||||
handleNotificationEvent(eventName, payload) {
|
||||
if (!eventName)
|
||||
return false;
|
||||
|
||||
const normalizedEvent = typeof eventName === 'string' ? eventName.toLowerCase() : '';
|
||||
switch (normalizedEvent) {
|
||||
case 'chargeradded':
|
||||
case 'chargerchanged':
|
||||
this.upsertCharger(payload);
|
||||
return true;
|
||||
case 'chargerremoved':
|
||||
this.removeCharger(payload);
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
onAuthenticationSucceeded() {
|
||||
this.updateConnectionStatus('Connected', 'connected');
|
||||
this.updateSessionSummary();
|
||||
|
|
@ -481,9 +507,10 @@ class DashboardApp {
|
|||
|
||||
const seen = new Set();
|
||||
chargers.forEach(charger => {
|
||||
if (!charger || !charger.id)
|
||||
const key = this.getChargerKey(charger);
|
||||
if (!key)
|
||||
return;
|
||||
seen.add(charger.id);
|
||||
seen.add(key);
|
||||
this.upsertCharger(charger);
|
||||
});
|
||||
|
||||
|
|
@ -494,21 +521,24 @@ class DashboardApp {
|
|||
}
|
||||
|
||||
upsertCharger(charger) {
|
||||
if (!charger || !charger.id)
|
||||
const key = this.getChargerKey(charger);
|
||||
if (!key)
|
||||
return;
|
||||
|
||||
const hasExisting = this.chargers.has(charger.id);
|
||||
const previous = hasExisting ? this.chargers.get(charger.id) : {};
|
||||
const hasExisting = this.chargers.has(key);
|
||||
const previous = hasExisting ? this.chargers.get(key) : {};
|
||||
const merged = { ...previous, ...charger };
|
||||
this.chargers.set(charger.id, merged);
|
||||
merged.thingId = key;
|
||||
this.chargers.set(key, merged);
|
||||
this.syncChargerRow(merged, !hasExisting);
|
||||
}
|
||||
|
||||
syncChargerRow(charger, forceCreate = false) {
|
||||
if (!charger || !charger.id || !this.elements.chargerTableBody)
|
||||
const key = this.getChargerKey(charger);
|
||||
if (!charger || !key || !this.elements.chargerTableBody)
|
||||
return;
|
||||
|
||||
let row = this.findChargerRow(charger.id);
|
||||
let row = this.findChargerRow(key);
|
||||
if (!row || forceCreate) {
|
||||
if (row && row.parentElement)
|
||||
row.parentElement.removeChild(row);
|
||||
|
|
@ -528,7 +558,7 @@ class DashboardApp {
|
|||
|
||||
buildChargerRow(charger) {
|
||||
const row = document.createElement('tr');
|
||||
row.dataset.chargerId = charger.id;
|
||||
row.dataset.chargerId = this.getChargerKey(charger) || '';
|
||||
this.chargerColumns.forEach(column => {
|
||||
const cell = document.createElement('td');
|
||||
cell.dataset.column = column.key;
|
||||
|
|
@ -538,22 +568,13 @@ class DashboardApp {
|
|||
return row;
|
||||
}
|
||||
|
||||
findChargerRow(chargerId) {
|
||||
if (!this.elements.chargerTableBody || !chargerId)
|
||||
return null;
|
||||
|
||||
const normalizedId = typeof CSS !== 'undefined' && CSS.escape
|
||||
? CSS.escape(String(chargerId))
|
||||
: String(chargerId).replace(/"/g, '\\"');
|
||||
return this.elements.chargerTableBody.querySelector(`tr[data-charger-id="${normalizedId}"]`);
|
||||
}
|
||||
|
||||
removeCharger(chargerId) {
|
||||
if (!chargerId)
|
||||
removeCharger(identifier) {
|
||||
const key = this.getChargerKey(identifier);
|
||||
if (!key)
|
||||
return;
|
||||
|
||||
this.chargers.delete(chargerId);
|
||||
const row = this.findChargerRow(chargerId);
|
||||
this.chargers.delete(key);
|
||||
const row = this.findChargerRow(key);
|
||||
if (row && row.parentElement)
|
||||
row.parentElement.removeChild(row);
|
||||
|
||||
|
|
@ -573,6 +594,32 @@ class DashboardApp {
|
|||
this.toggleChargerEmptyState();
|
||||
}
|
||||
|
||||
findChargerRow(chargerId) {
|
||||
if (!this.elements.chargerTableBody || !chargerId)
|
||||
return null;
|
||||
|
||||
const normalizedId = typeof CSS !== 'undefined' && CSS.escape
|
||||
? CSS.escape(String(chargerId))
|
||||
: String(chargerId).replace(/"/g, '\\"');
|
||||
return this.elements.chargerTableBody.querySelector(`tr[data-charger-id="${normalizedId}"]`);
|
||||
}
|
||||
|
||||
getChargerKey(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;
|
||||
}
|
||||
|
||||
toggleChargerEmptyState() {
|
||||
if (!this.elements.chargerEmptyRow)
|
||||
return;
|
||||
|
|
@ -680,7 +727,7 @@ class DashboardApp {
|
|||
|
||||
setAuthLayout(requireAuth) {
|
||||
const body = document.body;
|
||||
if (!body)
|
||||
if (!body || body.dataset.mode === 'help')
|
||||
return;
|
||||
body.classList.toggle('needs-auth', requireAuth);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,257 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>nymea EV Dash · Help</title>
|
||||
<style>
|
||||
:root {
|
||||
--primary-color: #0050a0;
|
||||
--secondary-color: #00a0e0;
|
||||
--accent-color: #f4b400;
|
||||
--surface-color: #ffffff;
|
||||
--background-color: #f5f7fa;
|
||||
--text-color: #1f2d3d;
|
||||
--muted-text-color: #566b84;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: "Segoe UI", Roboto, sans-serif;
|
||||
background: var(--background-color);
|
||||
color: var(--text-color);
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
header {
|
||||
background: linear-gradient(135deg, var(--primary-color), var(--secondary-color));
|
||||
color: #ffffff;
|
||||
padding: 2.5rem 1.5rem 2rem;
|
||||
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.header-bar {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.brand h1 {
|
||||
margin: 0 0 0.4rem;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.02em;
|
||||
}
|
||||
|
||||
.brand p {
|
||||
margin: 0;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.session-panel {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 1rem 1.5rem;
|
||||
}
|
||||
|
||||
.status-indicator {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
font-weight: 600;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.status-dot {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
border-radius: 50%;
|
||||
background-color: #d3dce6;
|
||||
transition: background-color 0.3s ease;
|
||||
}
|
||||
|
||||
.status-dot.connecting {
|
||||
background-color: #f4b400;
|
||||
}
|
||||
|
||||
.status-dot.connected {
|
||||
background-color: #2ecc71;
|
||||
}
|
||||
|
||||
.status-dot.authenticating {
|
||||
background-color: #8e44ad;
|
||||
}
|
||||
|
||||
.status-dot.error {
|
||||
background-color: #e74c3c;
|
||||
}
|
||||
|
||||
.tool-button {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 50%;
|
||||
border: 1px solid rgba(255, 255, 255, 0.65);
|
||||
color: #ffffff;
|
||||
background: transparent;
|
||||
text-decoration: none;
|
||||
font-weight: 600;
|
||||
font-size: 1.1rem;
|
||||
transition: background 0.15s ease, border-color 0.15s ease;
|
||||
}
|
||||
|
||||
.tool-button:hover {
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
border-color: #ffffff;
|
||||
}
|
||||
|
||||
.session-details {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
button.ghost {
|
||||
border: 1px solid rgba(255, 255, 255, 0.55);
|
||||
color: #ffffff;
|
||||
background: transparent;
|
||||
padding: 0.35rem 0.85rem;
|
||||
border-radius: 999px;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
button.ghost:hover {
|
||||
border-color: #ffffff;
|
||||
background: rgba(255, 255, 255, 0.12);
|
||||
}
|
||||
|
||||
main {
|
||||
flex: 1;
|
||||
width: min(960px, 92vw);
|
||||
margin: 2rem auto 3rem;
|
||||
display: grid;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.card {
|
||||
background: var(--surface-color);
|
||||
border-radius: 14px;
|
||||
padding: 1.5rem;
|
||||
box-shadow: 0 12px 32px rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
.card h2 {
|
||||
margin-top: 0;
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
.grid-two-column {
|
||||
display: grid;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
@media (min-width: 900px) {
|
||||
.grid-two-column {
|
||||
grid-template-columns: 1fr 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
pre {
|
||||
margin: 0;
|
||||
background: #f8fafc;
|
||||
border-radius: 10px;
|
||||
padding: 1rem;
|
||||
overflow-x: auto;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
code {
|
||||
font-family: "Fira Code", "SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace;
|
||||
}
|
||||
|
||||
.helper-text {
|
||||
font-size: 0.9rem;
|
||||
color: var(--muted-text-color);
|
||||
}
|
||||
|
||||
footer {
|
||||
text-align: center;
|
||||
padding: 1.25rem 1rem;
|
||||
background: #ffffff;
|
||||
border-top: 1px solid #d3dce6;
|
||||
font-size: 0.9rem;
|
||||
color: var(--muted-text-color);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body data-mode="help">
|
||||
<header>
|
||||
<div class="header-bar">
|
||||
<div class="brand">
|
||||
<h1>EV Dash</h1>
|
||||
<p>Reference & diagnostics</p>
|
||||
</div>
|
||||
<div class="session-panel">
|
||||
<p class="status-indicator">
|
||||
<span id="statusDot" class="status-dot connecting" aria-hidden="true"></span>
|
||||
<span id="connectionStatus">Awaiting login…</span>
|
||||
</p>
|
||||
<div class="session-details">
|
||||
<span id="sessionSummary">Load the dashboard to authenticate.</span>
|
||||
<button type="button" id="logoutButton" class="ghost hidden">Logout</button>
|
||||
</div>
|
||||
<a class="tool-button" href="index.html" aria-label="Back to dashboard">⟵</a>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main>
|
||||
<section class="card">
|
||||
<h2>API Contract</h2>
|
||||
<p class="helper-text">All requests follow the structure below. Use <code>app.sendAction(action, payload)</code> from the browser console after authenticating on the dashboard.</p>
|
||||
<div class="grid-two-column">
|
||||
<div>
|
||||
<h3>Request template</h3>
|
||||
<pre id="requestTemplate"></pre>
|
||||
</div>
|
||||
<div>
|
||||
<h3>Responses</h3>
|
||||
<pre id="responseTemplate"></pre>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="card" aria-live="polite">
|
||||
<h2>Last WebSocket message</h2>
|
||||
<p class="helper-text">Inspect the raw payload received from the nymea backend. Sign in on the dashboard first so this page can reuse the stored session.</p>
|
||||
<pre id="incomingMessage">No messages received yet.</pre>
|
||||
</section>
|
||||
|
||||
<section class="card">
|
||||
<h2>Charger table basics</h2>
|
||||
<ul>
|
||||
<li>The main dashboard creates a row for each charger ID and keeps it in sync with backend notifications.</li>
|
||||
<li>Columns follow the order defined by <code>EvDashEngine::packCharger</code>, so new properties appear automatically.</li>
|
||||
<li>Branding (colours, fonts) is driven by CSS variables near the top of each HTML file for easy theming.</li>
|
||||
</ul>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<footer>
|
||||
<span id="appVersion">nymea EV Dash</span> · © 2013–2025 chargebyte GmbH. All rights reserved.
|
||||
</footer>
|
||||
|
||||
<script src="app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -62,6 +62,27 @@
|
|||
gap: 1rem 1.5rem;
|
||||
}
|
||||
|
||||
.tool-button {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 50%;
|
||||
border: 1px solid rgba(255, 255, 255, 0.65);
|
||||
color: #ffffff;
|
||||
background: transparent;
|
||||
text-decoration: none;
|
||||
font-weight: 600;
|
||||
font-size: 1.1rem;
|
||||
transition: background 0.15s ease, border-color 0.15s ease;
|
||||
}
|
||||
|
||||
.tool-button:hover {
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
border-color: #ffffff;
|
||||
}
|
||||
|
||||
main {
|
||||
flex: 1;
|
||||
width: min(960px, 92vw);
|
||||
|
|
@ -341,13 +362,13 @@
|
|||
<span id="sessionSummary">Please sign in.</span>
|
||||
<button type="button" id="logoutButton" class="ghost hidden">Logout</button>
|
||||
</div>
|
||||
<a class="tool-button" href="help.html" aria-label="Open help">?</a>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<main>
|
||||
<section class="card">
|
||||
<h2>Chargers</h2>
|
||||
<p class="helper-text">Live data from your configured nymea EV chargers.</p>
|
||||
<div class="table-wrapper">
|
||||
<table class="chargers-table">
|
||||
<thead>
|
||||
|
|
@ -373,25 +394,6 @@
|
|||
</table>
|
||||
</div>
|
||||
</section>
|
||||
<section class="card">
|
||||
<h2>API Contract</h2>
|
||||
<p class="helper-text">All requests follow the structure below. Use <code>app.sendAction(action, payload)</code> from the browser console after authentication.</p>
|
||||
<div class="grid-two-column">
|
||||
<div>
|
||||
<h3>Request template</h3>
|
||||
<pre id="requestTemplate"></pre>
|
||||
</div>
|
||||
<div>
|
||||
<h3>Responses</h3>
|
||||
<pre id="responseTemplate"></pre>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="card" aria-live="polite">
|
||||
<h2>Last message</h2>
|
||||
<pre id="incomingMessage">No messages received yet.</pre>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<section id="loginOverlay" class="login-view" aria-labelledby="loginTitle">
|
||||
|
|
|
|||
|
|
@ -52,6 +52,19 @@ EvDashEngine::EvDashEngine(ThingManager *thingManager, EvDashWebServerResource *
|
|||
m_thingManager{thingManager},
|
||||
m_webServerResource{webServerResource}
|
||||
{
|
||||
Things evChargers = m_thingManager->configuredThings().filterByInterface("evcharger");
|
||||
|
||||
// Init charger list
|
||||
foreach (Thing *chargerThing, evChargers) {
|
||||
m_chargers.append(chargerThing);
|
||||
monitorChargerThing(chargerThing);
|
||||
}
|
||||
|
||||
connect(m_thingManager, &ThingManager::thingAdded, this, &EvDashEngine::onThingAdded);
|
||||
connect(m_thingManager, &ThingManager::thingRemoved, this, &EvDashEngine::onThingRemoved);
|
||||
connect(m_thingManager, &ThingManager::thingChanged, this, &EvDashEngine::onThingChanged);
|
||||
|
||||
// Setup websocket server
|
||||
m_webSocketServer = new QWebSocketServer(QStringLiteral("EvDashEngine"), QWebSocketServer::NonSecureMode, this);
|
||||
|
||||
connect(m_webSocketServer, &QWebSocketServer::newConnection, this, [this](){
|
||||
|
|
@ -100,6 +113,44 @@ EvDashEngine::~EvDashEngine()
|
|||
m_authenticatedClients.clear();
|
||||
}
|
||||
|
||||
void EvDashEngine::onThingAdded(Thing *thing)
|
||||
{
|
||||
if (thing->thingClass().interfaces().contains("evcharger")) {
|
||||
m_chargers.append(thing);
|
||||
monitorChargerThing(thing);
|
||||
sendNotification("ChargerAdded", packCharger(thing));
|
||||
}
|
||||
}
|
||||
|
||||
void EvDashEngine::onThingRemoved(const ThingId &thingId)
|
||||
{
|
||||
foreach (Thing *thing, m_chargers) {
|
||||
if (thing->id() == thingId) {
|
||||
qCDebug(dcEvDashExperience()) << "Charger has been removed.";
|
||||
m_chargers.removeAll(thing);
|
||||
sendNotification("ChargerRemoved", packCharger(thing));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void EvDashEngine::onThingChanged(Thing *thing)
|
||||
{
|
||||
sendNotification("ChargerChanged", packCharger(thing));
|
||||
}
|
||||
|
||||
void EvDashEngine::monitorChargerThing(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::startWebSocket(quint16 port)
|
||||
{
|
||||
if (m_webSocketServer->isListening()) {
|
||||
|
|
@ -232,6 +283,21 @@ void EvDashEngine::sendReply(QWebSocket *socket, QJsonObject response) const
|
|||
socket->sendTextMessage(QString::fromUtf8(replyDoc.toJson(QJsonDocument::Compact)));
|
||||
}
|
||||
|
||||
void EvDashEngine::sendNotification(const QString ¬ification, QJsonObject payload) const
|
||||
{
|
||||
// Send to all active clients
|
||||
|
||||
for (QWebSocket *client : qAsConst(m_clients)) {
|
||||
QJsonObject notificationObject;
|
||||
notificationObject.insert(QStringLiteral("requestId"), QUuid::createUuid().toString(QUuid::WithoutBraces));
|
||||
notificationObject.insert("event", notification);
|
||||
notificationObject.insert("payload", payload);
|
||||
const QJsonDocument notificationDoc(notificationObject);
|
||||
qCDebug(dcEvDashExperience()) << "<--" << qUtf8Printable(notificationDoc.toJson(QJsonDocument::Compact));
|
||||
client->sendTextMessage(QString::fromUtf8(notificationDoc.toJson(QJsonDocument::Compact)));
|
||||
}
|
||||
}
|
||||
|
||||
QJsonObject EvDashEngine::createSuccessResponse(const QString &requestId, const QJsonObject &payload) const
|
||||
{
|
||||
QJsonObject response;
|
||||
|
|
|
|||
|
|
@ -35,6 +35,8 @@
|
|||
#include <QHash>
|
||||
#include <QJsonObject>
|
||||
|
||||
#include <integrations/thing.h>
|
||||
|
||||
class QWebSocket;
|
||||
class QWebSocketServer;
|
||||
|
||||
|
|
@ -52,6 +54,11 @@ public:
|
|||
signals:
|
||||
void webSocketListeningChanged(bool listening);
|
||||
|
||||
private slots:
|
||||
void onThingAdded(Thing *thing);
|
||||
void onThingRemoved(const ThingId &thingId);
|
||||
void onThingChanged(Thing *thing);
|
||||
|
||||
private:
|
||||
ThingManager *m_thingManager = nullptr;
|
||||
EvDashWebServerResource *m_webServerResource = nullptr;
|
||||
|
|
@ -60,14 +67,23 @@ private:
|
|||
QList<QWebSocket *> m_clients;
|
||||
QHash<QWebSocket *, QString> m_authenticatedClients;
|
||||
|
||||
QList<Thing *> m_chargers;
|
||||
void monitorChargerThing(Thing *thing);
|
||||
|
||||
// Websocket server
|
||||
bool startWebSocket(quint16 port = 0);
|
||||
void processTextMessage(QWebSocket *socket, const QString &message);
|
||||
|
||||
// Websocket API
|
||||
QJsonObject handleApiRequest(QWebSocket *socket, const QJsonObject &request);
|
||||
void sendReply(QWebSocket *socket, QJsonObject response) const;
|
||||
void sendNotification(const QString ¬ification, QJsonObject payload) const;
|
||||
|
||||
QJsonObject createSuccessResponse(const QString &requestId, const QJsonObject &payload = {}) const;
|
||||
QJsonObject createErrorResponse(const QString &requestId, const QString &errorMessage) const;
|
||||
|
||||
QJsonObject packCharger(Thing *charger) const;
|
||||
|
||||
};
|
||||
|
||||
#endif // EVDASHENGINE_H
|
||||
|
|
|
|||
Loading…
Reference in New Issue