nymea-plugins/easee/signalrconnection.cpp

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