// 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 . * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ #include "signalrconnection.h" #include #include #include #include #include #include #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(&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 }); }