191 lines
7.2 KiB
C++
191 lines
7.2 KiB
C++
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
|
|
*
|
|
* Copyright 2013 - 2025, nymea GmbH
|
|
* Contact: contact@nymea.io
|
|
*
|
|
* This file is part of nymea.
|
|
* This project including source code and documentation is protected by
|
|
* copyright law, and remains the property of nymea GmbH. All rights, including
|
|
* reproduction, publication, editing and translation, are reserved. The use of
|
|
* this project is subject to the terms of a license agreement to be concluded
|
|
* with nymea GmbH in accordance with the terms of use of nymea GmbH, available
|
|
* under https://nymea.io/license
|
|
*
|
|
* GNU General Public License Usage
|
|
* Alternatively, this project may be redistributed and/or modified under the
|
|
* terms of the GNU General Public License as published by the Free Software
|
|
* Foundation, GNU version 3. This project is distributed in the hope that it
|
|
* will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty
|
|
* of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
|
|
* Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License along with
|
|
* this project. If not, see <https://www.gnu.org/licenses/>.
|
|
*
|
|
* For any further details and any questions please contact us under
|
|
* contact@nymea.io or see our FAQ/Licensing Information on
|
|
* https://nymea.io/license/faq
|
|
*
|
|
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
|
|
|
|
#include "evdashengine.h"
|
|
|
|
#include <integrations/thingmanager.h>
|
|
|
|
#include <QWebSocket>
|
|
#include <QWebSocketServer>
|
|
#include <QHostAddress>
|
|
|
|
#include <QDateTime>
|
|
#include <QJsonParseError>
|
|
#include <QJsonDocument>
|
|
|
|
#include <QLoggingCategory>
|
|
Q_DECLARE_LOGGING_CATEGORY(dcEvDashExperience)
|
|
|
|
EvDashEngine::EvDashEngine(ThingManager *thingManager, EvDashWebServerResource *webServerResource, QObject *parent)
|
|
: QObject{parent},
|
|
m_thingManager{thingManager},
|
|
m_webServerResource{webServerResource}
|
|
{
|
|
m_webSocketServer = new QWebSocketServer(QStringLiteral("EvDashEngine"), QWebSocketServer::NonSecureMode, this);
|
|
|
|
connect(m_webSocketServer, &QWebSocketServer::newConnection, this, [this](){
|
|
QWebSocket *socket = m_webSocketServer->nextPendingConnection();
|
|
if (!socket) {
|
|
qCWarning(dcEvDashExperience()) << "Interface: Received new connection but socket was null";
|
|
return;
|
|
}
|
|
|
|
connect(socket, &QWebSocket::textMessageReceived, this, [this, socket](const QString &message) {
|
|
processTextMessage(socket, message);
|
|
});
|
|
|
|
connect(socket, &QWebSocket::disconnected, this, [this, socket](){
|
|
m_clients.removeAll(socket);
|
|
qCDebug(dcEvDashExperience()) << "WebSocket client disconnected" << socket->peerAddress() << "Remaining clients:" << m_clients.count();
|
|
socket->deleteLater();
|
|
});
|
|
|
|
m_clients.append(socket);
|
|
qCDebug(dcEvDashExperience()) << "WebSocket client connected" << socket->peerAddress() << "Total clients:" << m_clients.count();
|
|
});
|
|
|
|
connect(m_webSocketServer, &QWebSocketServer::acceptError, this, [this](QAbstractSocket::SocketError error) {
|
|
qCWarning(dcEvDashExperience()) << "WebSocket accept error" << error << m_webSocketServer->errorString();
|
|
});
|
|
|
|
startWebSocket(4449);
|
|
}
|
|
|
|
EvDashEngine::~EvDashEngine()
|
|
{
|
|
if (m_webSocketServer->isListening())
|
|
m_webSocketServer->close();
|
|
|
|
for (QWebSocket *client : qAsConst(m_clients)) {
|
|
if (client->state() == QAbstractSocket::ConnectedState)
|
|
client->close(QWebSocketProtocol::CloseCodeGoingAway, QStringLiteral("Server shutting down"));
|
|
|
|
client->deleteLater();
|
|
}
|
|
|
|
m_clients.clear();
|
|
}
|
|
|
|
bool EvDashEngine::startWebSocket(quint16 port)
|
|
{
|
|
if (m_webSocketServer->isListening()) {
|
|
if (m_webSocketServer->serverPort() == port && port != 0)
|
|
return true;
|
|
|
|
m_webSocketServer->close();
|
|
}
|
|
|
|
const bool listening = m_webSocketServer->listen(QHostAddress::Any, port);
|
|
if (listening) {
|
|
qCDebug(dcEvDashExperience()) << "WebSocket server listening on" << m_webSocketServer->serverAddress() << m_webSocketServer->serverPort();
|
|
} else {
|
|
qCWarning(dcEvDashExperience()) << "Failed to start WebSocket server" << m_webSocketServer->errorString();
|
|
}
|
|
|
|
emit webSocketListeningChanged(listening);
|
|
return listening;
|
|
}
|
|
|
|
void EvDashEngine::processTextMessage(QWebSocket *socket, const QString &message)
|
|
{
|
|
if (!socket)
|
|
return;
|
|
|
|
QJsonParseError parseError;
|
|
const QJsonDocument doc = QJsonDocument::fromJson(message.toUtf8(), &parseError);
|
|
|
|
if (parseError.error != QJsonParseError::NoError || !doc.isObject()) {
|
|
qCWarning(dcEvDashExperience()) << "Invalid WebSocket payload" << parseError.errorString();
|
|
QJsonObject errorReply {
|
|
{QStringLiteral("version"), QStringLiteral("1.0")},
|
|
{QStringLiteral("event"), QStringLiteral("error")},
|
|
{QStringLiteral("payload"), QJsonObject{
|
|
{QStringLiteral("message"), QStringLiteral("Invalid JSON payload")},
|
|
{QStringLiteral("details"), parseError.errorString()}
|
|
}}
|
|
};
|
|
|
|
sendReply(socket, errorReply);
|
|
return;
|
|
}
|
|
|
|
const QJsonObject response = handleApiRequest(doc.object());
|
|
sendReply(socket, response);
|
|
}
|
|
|
|
QJsonObject EvDashEngine::handleApiRequest(const QJsonObject &request) const
|
|
{
|
|
qCDebug(dcEvDashExperience()) << "Handle API request" << request;
|
|
|
|
QJsonObject response;
|
|
response.insert(QStringLiteral("version"), request.value(QStringLiteral("version")).toString(QStringLiteral("1.0")));
|
|
|
|
const QString requestId = request.value(QStringLiteral("requestId")).toString();
|
|
if (!requestId.isEmpty())
|
|
response.insert(QStringLiteral("requestId"), requestId);
|
|
|
|
const QString action = request.value(QStringLiteral("action")).toString();
|
|
|
|
if (action.compare(QStringLiteral("ping"), Qt::CaseInsensitive) == 0) {
|
|
|
|
response.insert(QStringLiteral("event"), QStringLiteral("statusUpdate"));
|
|
|
|
QJsonObject payload;
|
|
payload.insert(QStringLiteral("status"), QStringLiteral("ok"));
|
|
payload.insert(QStringLiteral("timestamp"), QDateTime::currentDateTimeUtc().toString(Qt::ISODateWithMs));
|
|
|
|
const QJsonObject requestPayload = request.value(QStringLiteral("payload")).toObject();
|
|
if (!requestPayload.isEmpty())
|
|
payload.insert(QStringLiteral("echo"), requestPayload);
|
|
|
|
response.insert(QStringLiteral("payload"), payload);
|
|
} else {
|
|
response.insert(QStringLiteral("event"), QStringLiteral("error"));
|
|
QJsonObject payload;
|
|
payload.insert(QStringLiteral("message"), QStringLiteral("Unknown action"));
|
|
payload.insert(QStringLiteral("action"), action);
|
|
response.insert(QStringLiteral("payload"), payload);
|
|
}
|
|
|
|
return response;
|
|
}
|
|
|
|
void EvDashEngine::sendReply(QWebSocket *socket, QJsonObject response) const
|
|
{
|
|
if (!socket)
|
|
return;
|
|
|
|
if (!response.contains(QStringLiteral("version")))
|
|
response.insert(QStringLiteral("version"), QStringLiteral("1.0"));
|
|
|
|
const QJsonDocument replyDoc(response);
|
|
socket->sendTextMessage(QString::fromUtf8(replyDoc.toJson(QJsonDocument::Compact)));
|
|
}
|