diff --git a/shelly/integrationpluginshelly.cpp b/shelly/integrationpluginshelly.cpp index 63651320..90097682 100644 --- a/shelly/integrationpluginshelly.cpp +++ b/shelly/integrationpluginshelly.cpp @@ -426,7 +426,6 @@ void IntegrationPluginShelly::executeAction(ThingActionInfo *info) {shelly25Channel1ActionTypeId, 1}, {shelly25Channel2ActionTypeId, 2} }; - if (channelParamTypeMap.contains(thing->thingClassId())) { relay = thing->paramValue(channelParamTypeMap.value(thing->thingClassId())).toInt(); } else if (actionChannelMap.contains(action.actionTypeId())) { @@ -435,16 +434,27 @@ void IntegrationPluginShelly::executeAction(ThingActionInfo *info) ParamTypeId powerParamTypeId = powerActionParamTypesMap.value(action.actionTypeId()); bool on = action.param(powerParamTypeId).value().toBool(); - url.setPath(QString("/relay/%1").arg(relay - 1)); - QUrlQuery query; - query.addQueryItem("turn", on ? "on" : "off"); - url.setQuery(query); - QNetworkReply *reply = hardwareManager()->networkManager()->get(QNetworkRequest(url)); - connect(reply, &QNetworkReply::finished, &QNetworkReply::deleteLater); - connect(reply, &QNetworkReply::finished, info, [info, reply, on](){ - info->thing()->setStateValue("power", on); - info->finish(reply->error() == QNetworkReply::NoError ? Thing::ThingErrorNoError : Thing::ThingErrorHardwareFailure); - }); + + if (shellyId.contains("Plus")) { + QVariantMap params; + params.insert("id", relay - 1); + params.insert("on", on); + ShellyRpcReply *reply = m_rpcClients.value(thing)->sendRequest("Switch.Set", params); + connect(reply, &ShellyRpcReply::finished, info, [info](ShellyRpcReply::Status status, const QVariantMap &/*response*/){ + info->finish(status == ShellyRpcReply::StatusSuccess ? Thing::ThingErrorNoError : Thing::ThingErrorHardwareFailure); + }); + } else { + url.setPath(QString("/relay/%1").arg(relay - 1)); + QUrlQuery query; + query.addQueryItem("turn", on ? "on" : "off"); + url.setQuery(query); + QNetworkReply *reply = hardwareManager()->networkManager()->get(QNetworkRequest(url)); + connect(reply, &QNetworkReply::finished, &QNetworkReply::deleteLater); + connect(reply, &QNetworkReply::finished, info, [info, reply, on](){ + info->thing()->setStateValue("power", on); + info->finish(reply->error() == QNetworkReply::NoError ? Thing::ThingErrorNoError : Thing::ThingErrorHardwareFailure); + }); + } return; } @@ -1321,6 +1331,7 @@ void IntegrationPluginShelly::setupGen2(ThingSetupInfo *info) { Thing *thing = info->thing(); QHostAddress address = getIP(thing); + QString shellyId = info->thing()->paramValue("id").toString(); if (address.isNull()) { qCWarning(dcShelly()) << "Unable to determine Shelly's network address. Failed to set up device."; @@ -1328,11 +1339,15 @@ void IntegrationPluginShelly::setupGen2(ThingSetupInfo *info) return; } + QString password = info->thing()->paramValue("password").toString(); + ShellyJsonRpcClient *client = new ShellyJsonRpcClient(info->thing()); - client->open(address); + client->open(address, "admin", password, shellyId); connect(client, &ShellyJsonRpcClient::stateChanged, info, [info, client, this](QAbstractSocket::SocketState state) { qCDebug(dcShelly()) << "Websocket state changed:" << state; - ShellyRpcReply *reply = client->sendRequest("Shelly.GetDeviceInfo"); + // GetDeviceInfo wouldn't require authentication if enabled, so if the setup is changed to fetch some info from GetDeviceInfo, + // make sure to not just replace the GetStatus call, or authentication verification won't work any more. + ShellyRpcReply *reply = client->sendRequest("Shelly.GetStatus"); connect(reply, &ShellyRpcReply::finished, info, [info, client, this](ShellyRpcReply::Status status, const QVariantMap &response){ if (status != ShellyRpcReply::StatusSuccess) { qCWarning(dcShelly) << "Error during shelly setup"; @@ -1360,7 +1375,7 @@ void IntegrationPluginShelly::setupGen2(ThingSetupInfo *info) } if (state == QAbstractSocket::UnconnectedState) { - client->open(getIP(thing)); + client->open(getIP(thing), "admin", thing->paramValue("password").toString(), thing->paramValue("id").toString()); } }); connect(client, &ShellyJsonRpcClient::notificationReceived, thing, [thing, this](const QVariantMap ¬ification){ diff --git a/shelly/shellyjsonrpcclient.cpp b/shelly/shellyjsonrpcclient.cpp index 001e4903..588d3ad0 100644 --- a/shelly/shellyjsonrpcclient.cpp +++ b/shelly/shellyjsonrpcclient.cpp @@ -5,9 +5,9 @@ #include Q_DECLARE_LOGGING_CATEGORY(dcShelly) -ShellyRpcReply::ShellyRpcReply(int id, QObject *parent): +ShellyRpcReply::ShellyRpcReply(const QVariantMap &requestBody, QObject *parent): QObject(parent), - m_id(id) + m_requestBody(requestBody) { QTimer::singleShot(10000, this, [this]{finished(StatusTimeout, QVariantMap());}); connect(this, &ShellyRpcReply::finished, this, &ShellyRpcReply::deleteLater); @@ -15,7 +15,12 @@ ShellyRpcReply::ShellyRpcReply(int id, QObject *parent): int ShellyRpcReply::id() const { - return m_id; + return m_requestBody.value("id").toInt(); +} + +QVariantMap ShellyRpcReply::requestBody() const +{ + return m_requestBody; } ShellyJsonRpcClient::ShellyJsonRpcClient(QObject *parent) @@ -27,8 +32,12 @@ ShellyJsonRpcClient::ShellyJsonRpcClient(QObject *parent) connect(m_socket, &QWebSocket::textMessageReceived, this, &ShellyJsonRpcClient::onTextMessageReceived); } -void ShellyJsonRpcClient::open(const QHostAddress &address) +void ShellyJsonRpcClient::open(const QHostAddress &address, const QString &user, const QString &password, const QString &shellyId) { + m_password = password; + m_user = user; + m_shellyId = shellyId; + QUrl url; url.setScheme("ws"); url.setHost(address.toString()); @@ -36,7 +45,7 @@ void ShellyJsonRpcClient::open(const QHostAddress &address) m_socket->open(url); } -ShellyRpcReply *ShellyJsonRpcClient::sendRequest(const QString &method) +ShellyRpcReply *ShellyJsonRpcClient::sendRequest(const QString &method, const QVariantMap ¶ms) { int id = m_currentId++; @@ -44,8 +53,15 @@ ShellyRpcReply *ShellyJsonRpcClient::sendRequest(const QString &method) data.insert("id", id); data.insert("src", "nymea"); data.insert("method", method); + if (!params.isEmpty()) { + data.insert("params", params); + } - ShellyRpcReply *reply = new ShellyRpcReply(id, this); + if (!m_password.isEmpty() && m_nonce != 0) { + data.insert("auth", createAuthMap()); + } + + ShellyRpcReply *reply = new ShellyRpcReply(data, this); connect(reply, &ShellyRpcReply::finished, this, [this, id]{ m_pendingReplies.remove(id); }); @@ -75,12 +91,62 @@ void ShellyJsonRpcClient::onTextMessageReceived(const QString &message) } int id = data.value("id").toInt(); - ShellyRpcReply *reply = m_pendingReplies.take(id); + ShellyRpcReply *reply = m_pendingReplies.value(id); if (!reply) { qCDebug(dcShelly()) << "Received a message which is neither a notification nor a reply to a request:" << message; return; } - reply->finished(ShellyRpcReply::StatusSuccess, data.value("result").toMap()); + ShellyRpcReply::Status status = ShellyRpcReply::StatusSuccess; + if (data.contains("error")) { + QVariantMap errorMap = data.value("error").toMap(); + qCWarning(dcShelly()) << "Error in shelly command:" << errorMap.value("code").toInt() << errorMap.value("message"); + status = static_cast(errorMap.value("code").toInt()); + + if (status == ShellyRpcReply::StatusAuthenticationRequired) { + if (m_nonce == 0) { + qCInfo(dcShelly) << "Authentication required. Initializing nonce and retrying..."; + + QJsonParseError error; + QVariantMap authInfo = QJsonDocument::fromJson(errorMap.value("message").toByteArray(), &error).toVariant().toMap(); + if (error.error != QJsonParseError::NoError) { + qCWarning(dcShelly()) << "Unable to parse auth error message. Authentication will not work."; + emit reply->finished(status, QVariantMap()); + return; + } + m_nonce = authInfo.value("nonce").toInt(); + m_nc = authInfo.value("nc").toInt(); + QVariantMap newBody = reply->requestBody(); + newBody.insert("auth", createAuthMap()); + qCDebug(dcShelly) << "Sending request with auth" << qUtf8Printable(QJsonDocument::fromVariant(newBody).toJson()); + m_socket->sendTextMessage(QJsonDocument::fromVariant(newBody).toJson(QJsonDocument::Compact)); + return; + } else { + qCWarning(dcShelly()) << "Username and password seem to be wrong."; + } + } + } + + emit reply->finished(status, data.value("result").toMap()); +} + +QVariantMap ShellyJsonRpcClient::createAuthMap() const +{ + int cnonce = qrand(); + QByteArray ha1 = QString("%1:%2:%3").arg(m_user).arg(m_shellyId.toLower()).arg(m_password).toUtf8(); + ha1 = QCryptographicHash::hash(ha1, QCryptographicHash::Sha256).toHex(); + QByteArray ha2 = QByteArrayLiteral("dummy_method:dummy_uri"); + ha2 = QCryptographicHash::hash(ha2, QCryptographicHash::Sha256).toHex(); + QByteArray response = QString("%1:%2:%3:%4:auth:%5").arg(QString::fromUtf8(ha1)).arg(m_nonce).arg(m_nc).arg(cnonce).arg(QString::fromUtf8(ha2)).toUtf8(); + response = QCryptographicHash::hash(response, QCryptographicHash::Sha256).toHex(); + + QVariantMap auth; + auth.insert("realm", m_shellyId.toLower()); + auth.insert("username", m_user); + auth.insert("nonce", m_nonce); + auth.insert("cnonce", cnonce); + auth.insert("response", response); + auth.insert("algorithm", "SHA-256"); + return auth; } diff --git a/shelly/shellyjsonrpcclient.h b/shelly/shellyjsonrpcclient.h index 5a1c2d02..a19f1982 100644 --- a/shelly/shellyjsonrpcclient.h +++ b/shelly/shellyjsonrpcclient.h @@ -9,20 +9,35 @@ class ShellyRpcReply: public QObject Q_OBJECT public: enum Status { - StatusSuccess, - StatusTimeout + StatusSuccess = 0, + + // Shelly common error codes + StatusInvalidArgument = -103, + StatusDeadlineExceeded = -104, + StatusResourceExhausted = -108, + StatusFailedPrecondition = -109, + StatusUnavailable = -114, + + StatusCodeBadRequest = 400, + StatusAuthenticationRequired = 401, + + // Our own + StatusTimeout = -1 + }; Q_ENUM(Status) - explicit ShellyRpcReply(int id, QObject *parent = nullptr); + explicit ShellyRpcReply(const QVariantMap &requestBody, QObject *parent = nullptr); int id() const; + QString method() const; + QVariantMap requestBody() const; signals: void finished(Status status, const QVariantMap &response); private: - int m_id = 0; + QVariantMap m_requestBody; }; class ShellyJsonRpcClient : public QObject @@ -31,9 +46,9 @@ class ShellyJsonRpcClient : public QObject public: explicit ShellyJsonRpcClient(QObject *parent = nullptr); - void open(const QHostAddress &address); + void open(const QHostAddress &address, const QString &user, const QString &password, const QString &shellyId); - ShellyRpcReply* sendRequest(const QString &method); + ShellyRpcReply* sendRequest(const QString &method, const QVariantMap ¶ms = QVariantMap()); signals: void stateChanged(QAbstractSocket::SocketState state); @@ -43,10 +58,19 @@ private slots: void onTextMessageReceived(const QString &message); private: + QVariantMap createAuthMap() const; + QWebSocket *m_socket = nullptr; QHash m_pendingReplies; int m_currentId = 1; + + // Needed (only) for authentication + QString m_user; + QString m_password; + QString m_shellyId; + qulonglong m_nonce = 0; + int m_nc = 0; }; #endif // SHELLYJSONRPCCLIENT_H