202 lines
7.5 KiB
C++
202 lines
7.5 KiB
C++
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
|
|
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
|
|
*
|
|
* Copyright (C) 2013 - 2024, nymea GmbH
|
|
* Copyright (C) 2024 - 2025, chargebyte austria GmbH
|
|
*
|
|
* This file is part of nymea-plugins.
|
|
*
|
|
* nymea-plugins is free software: you can redistribute it and/or modify
|
|
* it under the terms of the GNU General Public License as published by
|
|
* the Free Software Foundation, either version 3 of the License, or
|
|
* (at your option) any later version.
|
|
*
|
|
* nymea-plugins 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 nymea-plugins. If not, see <https://www.gnu.org/licenses/>.
|
|
*
|
|
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
|
|
|
|
#include "signalrconnection.h"
|
|
|
|
#include <QNetworkRequest>
|
|
#include <QWebSocket>
|
|
#include <QJsonDocument>
|
|
#include <QNetworkReply>
|
|
#include <QUrlQuery>
|
|
#include <QTimer>
|
|
|
|
#include "extern-plugininfo.h"
|
|
|
|
SignalRConnection::SignalRConnection(const QUrl &url, const QByteArray &accessToken, NetworkAccessManager *nam, QObject *parent)
|
|
: QObject{parent},
|
|
m_url(url),
|
|
m_accessToken(accessToken),
|
|
m_nam(nam)
|
|
{
|
|
m_socket = new QWebSocket();
|
|
typedef void (QWebSocket:: *errorSignal)(QAbstractSocket::SocketError);
|
|
connect(m_socket, static_cast<errorSignal>(&QWebSocket::error), this, [](QAbstractSocket::SocketError error){
|
|
qCWarning(dcEasee) << "SingalR: Error in websocket:" << error;
|
|
});
|
|
connect(m_socket, &QWebSocket::stateChanged, this, [=](QAbstractSocket::SocketState state){
|
|
qCDebug(dcEasee) << "SingalR: Websocket state changed" << state;
|
|
|
|
if (state == QAbstractSocket::ConnectedState) {
|
|
qCDebug(dcEasee) << "SingalR: Websocket connected";
|
|
|
|
QVariantMap handshake;
|
|
handshake.insert("protocol", "json");
|
|
handshake.insert("version", 1);
|
|
QByteArray data = encode(handshake);
|
|
qCDebug(dcEasee) << "Sending handshake" << data;
|
|
m_socket->sendTextMessage(data);
|
|
m_watchdog->start();
|
|
} else if (QAbstractSocket::UnconnectedState) {
|
|
m_watchdog->stop();
|
|
QTimer::singleShot(5000, this, [=](){
|
|
connectToHost();
|
|
});
|
|
}
|
|
|
|
});
|
|
connect(m_socket, &QWebSocket::binaryMessageReceived, this, [](const QByteArray &message){
|
|
qCDebug(dcEasee) << "SingalR: Binary message received" << message;
|
|
});
|
|
connect(m_socket, &QWebSocket::textMessageReceived, this, [=](const QString &message){
|
|
qCDebug(dcEasee) << "SingalR: Text message received" << message;
|
|
|
|
QStringList messages = message.split(QByteArray::fromHex("1E"));
|
|
|
|
foreach (const QString &msg, messages) {
|
|
if (msg.isEmpty()) {
|
|
continue;
|
|
}
|
|
// qCDebug(dcEasee()) << "Received message:" << msg;
|
|
|
|
QJsonParseError error;
|
|
QJsonDocument jsonDoc = QJsonDocument::fromJson(msg.toUtf8(), &error);
|
|
if (error.error != QJsonParseError::NoError) {
|
|
qCWarning(dcEasee()) << "SingalR: Unable to parse message from SignalR socket" << error.errorString() << msg;
|
|
continue;
|
|
}
|
|
|
|
if (m_waitingForHandshakeReply && jsonDoc.toVariant().toMap().isEmpty()) {
|
|
m_waitingForHandshakeReply = false;
|
|
qCDebug(dcEasee()) << "SingalR: Handshake reply received.";
|
|
emit connectionStateChanged(true);
|
|
return;
|
|
}
|
|
|
|
QVariantMap map = jsonDoc.toVariant().toMap();
|
|
switch (map.value("type").toUInt()) {
|
|
case 1:
|
|
emit dataReceived(map);
|
|
break;
|
|
case 3:
|
|
// Silencing acks to our requests
|
|
qCDebug(dcEasee()) << "SingalR: Message ACK received:" << map;
|
|
break;
|
|
case 6:
|
|
// Resetting watchdog
|
|
m_watchdog->start();
|
|
break;
|
|
default:
|
|
qCWarning(dcEasee()) << "SingalR: Unhandled SingalR message type" << map;
|
|
}
|
|
}
|
|
});
|
|
|
|
connectToHost();
|
|
|
|
m_watchdog = new QTimer(this);
|
|
m_watchdog->setInterval(30000);
|
|
connect(m_watchdog, &QTimer::timeout, this, [=](){
|
|
qCWarning(dcEasee()) << "SingalR: Watchdog triggered! Reconnecting web socket stream...";
|
|
m_socket->close();
|
|
connectToHost();
|
|
});
|
|
}
|
|
|
|
void SignalRConnection::subscribe(const QString &chargerId)
|
|
{
|
|
QVariantMap map;
|
|
map.insert("type", 1);
|
|
map.insert("invocationId", QUuid::createUuid());
|
|
map.insert("target", "SubscribeWithCurrentState");
|
|
map.insert("arguments", QVariantList{chargerId, true});
|
|
qCDebug(dcEasee) << "SingalR: subscribing to" << chargerId;
|
|
m_socket->sendTextMessage(encode(map));
|
|
}
|
|
|
|
bool SignalRConnection::connected() const
|
|
{
|
|
return m_socket->state() == QAbstractSocket::ConnectedState;
|
|
}
|
|
|
|
void SignalRConnection::updateToken(const QByteArray &accessToken)
|
|
{
|
|
m_accessToken = accessToken;
|
|
m_socket->close();
|
|
connectToHost();
|
|
}
|
|
|
|
QByteArray SignalRConnection::encode(const QVariantMap &message)
|
|
{
|
|
return QJsonDocument::fromVariant(message).toJson(QJsonDocument::Compact).append(QByteArray::fromHex("1E"));
|
|
}
|
|
|
|
void SignalRConnection::connectToHost()
|
|
{
|
|
QUrl negotiationUrl = m_url;
|
|
negotiationUrl.setScheme("https");
|
|
negotiationUrl.setPath(negotiationUrl.path() + "/negotiate");
|
|
QNetworkRequest negotiateRequest(negotiationUrl);
|
|
negotiateRequest.setRawHeader("Authorization", "Bearer " + m_accessToken);
|
|
qCDebug(dcEasee()) << "SingalR: Negotiating:" << negotiationUrl << negotiateRequest.rawHeader("Authorization");
|
|
QNetworkReply *negotiantionReply = m_nam->post(negotiateRequest, QByteArray());
|
|
connect(negotiantionReply, &QNetworkReply::finished, this, [=](){
|
|
if (negotiantionReply->error() != QNetworkReply::NoError) {
|
|
qCWarning(dcEasee()) << "SingalR: Unable to neotiate SignalR channel:" << negotiantionReply->error();
|
|
QTimer::singleShot(5000, this, [=](){connectToHost();});
|
|
return;
|
|
}
|
|
QByteArray data = negotiantionReply->readAll();
|
|
qCDebug(dcEasee) << "SingalR: Negotiation reply" << data;
|
|
QJsonParseError error;
|
|
QJsonDocument jsonDoc = QJsonDocument::fromJson(data, &error);
|
|
if (error.error != QJsonParseError::NoError) {
|
|
qCWarning(dcEasee()) << "SingalR: Unable to parse json from negoatiate endpoint" << error.errorString() << data;
|
|
QTimer::singleShot(5000, this, [=](){connectToHost();});
|
|
return;
|
|
}
|
|
|
|
QVariantMap map = jsonDoc.toVariant().toMap();
|
|
QString connectionId = map.value("connectionId").toString();
|
|
|
|
|
|
QUrl wsUrl = m_url;
|
|
wsUrl.setScheme("wss");
|
|
QUrlQuery query;
|
|
query.addQueryItem("id", connectionId);
|
|
wsUrl.setQuery(query);
|
|
QNetworkRequest request(wsUrl);
|
|
request.setRawHeader("Authorization", "Bearer " + m_accessToken);
|
|
qCDebug(dcEasee()) << "SingalR: Connecting websocket:" << wsUrl.toString();
|
|
m_waitingForHandshakeReply = true;
|
|
#if QT_VERSION >= QT_VERSION_CHECK(5,6,0)
|
|
m_socket->open(request);
|
|
#else
|
|
qCWarning(dcEasee()) << "This plugin requires at least Qt 5.6 to establish a signal R connection. Updating values won't work.";
|
|
#endif
|
|
|
|
});
|
|
|
|
}
|
|
|