/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * Copyright 2013 - 2020, 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 Lesser General Public License Usage * Alternatively, this project may be redistributed and/or modified under the * terms of the GNU Lesser General Public License as published by the Free * Software Foundation; 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 * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this project. If not, see . * * 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 "miele.h" #include "extern-plugininfo.h" #include #include #include #include #include #include #include Miele::Miele(NetworkAccessManager *networkmanager, const QByteArray &clientId, const QByteArray &clientSecret, const QString &language, QObject *parent) : QObject(parent), m_language(language), m_clientId(clientId), m_clientSecret(clientSecret), m_networkManager(networkmanager) { m_tokenRefreshTimer = new QTimer(this); m_tokenRefreshTimer->start(m_refreshInterval*1000); // 10 minutes connect(m_tokenRefreshTimer, &QTimer::timeout, this, &Miele::onRefreshTimeout); } QByteArray Miele::accessToken() { return m_accessToken; } QByteArray Miele::refreshToken() { return m_refreshToken; } QUrl Miele::getLoginUrl(const QUrl &redirectUrl, const QString &state) { if (m_clientId.isEmpty()) { qCWarning(dcMiele) << "Client key not defined!"; return QUrl(""); } if (redirectUrl.isEmpty()){ qCWarning(dcMiele()) << "No redirect uri defined!"; } m_redirectUri = QUrl::toPercentEncoding(redirectUrl.toString()); QUrl url(m_authorizationUrl); QUrlQuery queryParams; queryParams.addQueryItem("client_id", m_clientId); queryParams.addQueryItem("redirect_uri", m_redirectUri); queryParams.addQueryItem("response_type", "code"); if (!state.isEmpty()) queryParams.addQueryItem("state", state); url.setQuery(queryParams); return url; } void Miele::getAccessTokenFromRefreshToken(const QByteArray &refreshToken) { qCDebug(dcMiele()) << "Getting access token from refresh token"; if (refreshToken.isEmpty()) { qCWarning(dcMiele()) << "No refresh token given!"; setAuthenticated(false); return; } QUrl url(m_tokenUrl); QUrlQuery query; query.clear(); query.addQueryItem("grant_type", "refresh_token"); query.addQueryItem("refresh_token", refreshToken); query.addQueryItem("client_id", m_clientId); query.addQueryItem("client_secret", m_clientSecret); QNetworkRequest request(url); request.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded"); QNetworkReply *reply = m_networkManager->post(request, query.toString().toUtf8()); connect(reply, &QNetworkReply::finished, reply, &QNetworkReply::deleteLater); connect(reply, &QNetworkReply::finished, this, [this, reply, refreshToken](){ QByteArray rawData = reply->readAll(); if (!checkStatusCode(reply, rawData)) { return; } QJsonDocument data = QJsonDocument::fromJson(rawData); if(!data.toVariant().toMap().contains("access_token") || !data.toVariant().toMap().contains("refresh_token")) { setAuthenticated(false); return; } m_accessToken = data.toVariant().toMap().value("access_token").toByteArray(); emit receivedAccessToken(m_accessToken); m_refreshToken = data.toVariant().toMap().value("refresh_token").toByteArray(); emit receivedRefreshToken(m_refreshToken); if (data.toVariant().toMap().contains("expires_in")) { int expireTime = data.toVariant().toMap().value("expires_in").toInt(); qCDebug(dcMiele) << "Access token expires int" << expireTime << "s, at" << QDateTime::currentDateTime().addSecs(expireTime).toString(); if (expireTime < m_refreshInterval) { qCWarning(dcMiele()) << "Expire time too short"; return; } m_accessTokenExpireTime = QDateTime::currentMSecsSinceEpoch() + (expireTime-(m_refreshInterval*1000)); } }); } void Miele::getAccessTokenFromAuthorizationCode(const QByteArray &authorizationCode) { qCDebug(dcMiele()) << "Getting accsss token from authorization code"; if(authorizationCode.isEmpty()) qCWarning(dcMiele) << "No authorization code given!"; QUrl url = QUrl(m_tokenUrl); QUrlQuery query; query.clear(); query.addQueryItem("client_id", m_clientId); query.addQueryItem("client_secret", m_clientSecret); query.addQueryItem("vg", "de-DE"); query.addQueryItem("redirect_uri", m_redirectUri); query.addQueryItem("grant_type", "authorization_code"); query.addQueryItem("code", authorizationCode); url.setQuery(query); QNetworkRequest request(url); request.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded"); QNetworkReply *reply = m_networkManager->post(request, query.toString().toUtf8()); connect(reply, &QNetworkReply::finished, reply, &QNetworkReply::deleteLater); connect(reply, &QNetworkReply::finished, this, [this, reply](){ QByteArray rawData = reply->readAll(); if (!checkStatusCode(reply, rawData)) { return; } QJsonDocument jsonDoc = QJsonDocument::fromJson(rawData); if(!jsonDoc.toVariant().toMap().contains("access_token") || !jsonDoc.toVariant().toMap().contains("refresh_token") ) { setAuthenticated(false); return; } m_accessToken = jsonDoc.toVariant().toMap().value("access_token").toByteArray(); receivedAccessToken(m_accessToken); m_refreshToken = jsonDoc.toVariant().toMap().value("refresh_token").toByteArray(); receivedRefreshToken(m_refreshToken); if (jsonDoc.toVariant().toMap().contains("expires_in")) { int expireTime = jsonDoc.toVariant().toMap().value("expires_in").toUInt(); qCDebug(dcMiele()) << "Token expires in" << expireTime << "s, at" << QDateTime::currentDateTime().addSecs(expireTime).toString(); if (expireTime < m_refreshInterval) { qCWarning(dcMiele()) << "Expire time too short"; return; } m_accessTokenExpireTime = QDateTime::currentMSecsSinceEpoch() + (expireTime-(m_refreshInterval*1000)); } }); } void Miele::getDevices() { qCDebug(dcMiele()) << "Get devices"; QUrl url = m_apiUrl; url.setPath("/v1/devices"); url.setQuery("language=en"); QNetworkRequest request(url); request.setRawHeader("Authorization", "Bearer "+m_accessToken); request.setRawHeader("accept", "application/json; charset=utf-8"); qCDebug(dcMiele()) << "Sending GET request" << request.url() << request.rawHeader("Authorization"); QNetworkReply *reply = m_networkManager->get(request); connect(reply, &QNetworkReply::finished, reply, &QNetworkReply::deleteLater); connect(reply, &QNetworkReply::finished, this, [this, reply] { QByteArray rawData = reply->readAll(); if (!checkStatusCode(reply, rawData)) { return; } QVariantMap map = QJsonDocument::fromJson(rawData).toVariant().toMap(); if (map.contains("data")) { QVariantMap dataMap = map.value("data").toMap(); qCDebug(dcMiele()) << "key" << dataMap.value("key").toString() << "value" << dataMap.value("value").toString() << dataMap.value("unit").toString(); } else if (map.contains("error")) { qCWarning(dcMiele()) << "Send command" << map.value("error").toMap().value("description").toString(); } }); } void Miele::getDevicesShort() { qCDebug(dcMiele()) << "Get devices short"; QUrl url = m_apiUrl; url.setPath("/v1/short/devices"); url.setQuery("language=en"); QNetworkRequest request(url); request.setRawHeader("Authorization", "Bearer "+m_accessToken); request.setRawHeader("accept", "application/json; charset=utf-8"); qCDebug(dcMiele()) << "Sending GET request" << request.url() << request.rawHeader("Authorization"); QNetworkReply *reply = m_networkManager->get(request); connect(reply, &QNetworkReply::finished, reply, &QNetworkReply::deleteLater); connect(reply, &QNetworkReply::finished, this, [this, reply] { QByteArray rawData = reply->readAll(); if (!checkStatusCode(reply, rawData)) { return; } QVariantList devices = QJsonDocument::fromJson(rawData).toVariant().toList(); QList foundDevices; foreach (QVariant device, devices) { QVariantMap deviceObj = device.toMap(); qCDebug(dcMiele()) << "Got device: " << deviceObj; DeviceShort ds; ds.fabNumber = deviceObj["fabNumber"].toString(); ds.name = deviceObj["deviceName"].toString(); ds.state = deviceObj["state"].toString(); ds.type = deviceObj["type"].toString(); foundDevices.append(ds); } emit devicesFound(foundDevices); }); } void Miele::getDevice(const QString &deviceId) { qCDebug(dcMiele()) << "Get device" << deviceId; QUrl url = m_apiUrl; url.setPath("/v1/devices/" + deviceId); url.setQuery("language=en"); QNetworkRequest request(url); request.setRawHeader("Authorization", "Bearer "+m_accessToken); request.setRawHeader("accept", "application/json; charset=utf-8"); QNetworkReply *reply = m_networkManager->get(request); connect(reply, &QNetworkReply::finished, reply, &QNetworkReply::deleteLater); connect(reply, &QNetworkReply::finished, this, [this, reply]{ QByteArray rawData = reply->readAll(); if (!checkStatusCode(reply, rawData)) { return; } QVariantMap map = QJsonDocument::fromJson(rawData).toVariant().toMap(); qCDebug(dcMiele()) << "Got device: " << map; }); } void Miele::getDeviceState(const QString &deviceId) { qCDebug(dcMiele()) << "Get device state for: " << deviceId; QUrl url = m_apiUrl; url.setPath("/v1/devices/" + deviceId + "/state"); url.setQuery("language=en"); QNetworkRequest request(url); request.setRawHeader("Authorization", "Bearer "+m_accessToken); request.setRawHeader("accept", "application/json; charset=utf-8"); QNetworkReply *reply = m_networkManager->get(request); connect(reply, &QNetworkReply::finished, reply, &QNetworkReply::deleteLater); connect(reply, &QNetworkReply::finished, this, [this, reply, deviceId]{ QByteArray rawData = reply->readAll(); if (!checkStatusCode(reply, rawData)) { int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); if (status == 404) { emit deviceNotFound(deviceId); } return; } QVariantMap map = QJsonDocument::fromJson(rawData).toVariant().toMap(); qCDebug(dcMiele()) << "Got device state for: " << deviceId; emit deviceStateReceived(deviceId, map); }); } void Miele::getActions(const QString &deviceId) { qCDebug(dcMiele()) << "Get actions" << deviceId; //TODO } QUuid Miele::processAction(const QString &deviceId, Miele::ProcessAction action) { qCDebug(dcMiele()) << "Process action" << deviceId << action; QJsonDocument doc; QJsonObject object; object.insert("processAction", action); doc.setObject(object); return putAction(deviceId, doc); } QUuid Miele::setPower(const QString &deviceId, bool power) { QJsonDocument doc; QJsonObject object; if (power) { object.insert("powerOn", true); } else { object.insert("powerOff", true); } doc.setObject(object); return putAction(deviceId, doc); } QUuid Miele::setDeviceName(const QString &deviceId, const QString &deviceName) { QJsonDocument doc; QJsonObject object; object.insert("description", deviceName); doc.setObject(object); return putAction(deviceId, doc); } QUuid Miele::setLight(const QString &deviceId, bool power) { QJsonDocument doc; QJsonObject object; if (power) { object.insert("light", 1); } else { object.insert("light", 2); } doc.setObject(object); return putAction(deviceId, doc); } QUuid Miele::setTargetTemperature(const QString &deviceId, int zone, int targetTemperature) { QJsonDocument doc; QJsonObject object; QJsonObject temperatureObj; temperatureObj.insert("zone", zone); temperatureObj.insert("value", targetTemperature); QJsonArray tempZones; tempZones.push_front(temperatureObj); object.insert("targetTemperature", tempZones); doc.setObject(object); return putAction(deviceId, doc); } QUuid Miele::setColors(const QString &deviceId, Miele::Color color) { QJsonDocument doc; QJsonObject object; QString colorString = QMetaEnum::fromType().valueToKey(color); object.insert("color", colorString.remove("Color").toLower()); doc.setObject(object); return putAction(deviceId, doc); } QUuid Miele::setColorsStr(const QString &deviceId, const QString &color) { QJsonDocument doc; QJsonObject object; object.insert("colors", color.toLower()); doc.setObject(object); return putAction(deviceId, doc); } QUuid Miele::setModes(const QString &deviceId, Miele::Mode mode) { QJsonDocument doc; QJsonObject object; object.insert("modes", mode); doc.setObject(object); return putAction(deviceId, doc); } QUuid Miele::setVentilationStep(const QString &deviceId, int step) { QJsonDocument doc; QJsonObject object; object.insert("ventilationStep", step); doc.setObject(object); return putAction(deviceId, doc); } QUuid Miele::setStartTime(const QString &deviceId, int seconds) { QJsonDocument doc; QJsonObject object; object.insert("startTime", seconds); doc.setObject(object); return putAction(deviceId, doc); } void Miele::connectEventStream() { QUrl url = m_apiUrl; url.setPath("/v1/devices/all/events"); QNetworkRequest request(url); request.setRawHeader("Authorization", "Bearer "+m_accessToken); request.setRawHeader("accept", "text/event-stream"); QNetworkReply *reply = m_networkManager->get(request); connect(reply, &QNetworkReply::finished, [reply, this] { reply->deleteLater(); QTimer::singleShot(5000, this, [this] {connectEventStream();}); //try to reconnect every 5 seconds }); connect(reply, &QNetworkReply::readyRead, this, [this, reply]{ while (reply->canReadLine()) { QByteArray rawData = reply->readAll(); qCDebug(dcMiele()) << "Raw events data: " << rawData; QJsonDocument data = QJsonDocument::fromJson(rawData); qCDebug(dcMiele()) << "Got events data: " << data.toJson(); QVariantMap eventsData = data.toVariant().toMap(); foreach (QVariant eventEntry, eventsData.values()) { QVariantMap eventEntryMap = eventEntry.toMap(); if (!eventEntryMap.contains("ident")) { qCWarning(dcMiele()) << "Got event but no device ident available"; return; } QVariantMap deviceIdent = eventEntryMap.value("ident").toMap().value("deviceIdentLabel").toMap(); QString deviceId = deviceIdent.value("fabNumber").toString(); QVariantMap state = eventEntryMap.value("state").toMap(); emit deviceStateReceived(deviceId, state); } } }); } QUuid Miele::putAction(const QString &deviceId, const QJsonDocument &action) { QUuid commandId = QUuid::createUuid(); QUrl url = m_apiUrl; url.setPath("/v1/devices/"+deviceId+"/actions"); url.setQuery("language=en"); QNetworkRequest request(url); request.setRawHeader("Authorization", "Bearer "+m_accessToken); // request.setRawHeader("Accept-Language", "en-US"); request.setRawHeader("accept", "application/json; charset=utf-8"); request.setHeader(QNetworkRequest::KnownHeaders::ContentTypeHeader, "application/json"); QNetworkReply *reply = m_networkManager->put(request, action.toJson()); connect(reply, &QNetworkReply::finished, reply, &QNetworkReply::deleteLater); connect(reply, &QNetworkReply::finished, this, [this, commandId, reply]{ QByteArray rawData = reply->readAll(); if (!checkStatusCode(reply, rawData)) { return; } QVariantMap map = QJsonDocument::fromJson(rawData).toVariant().toMap(); qCDebug(dcMiele()) << "Send command" << map; if (map.contains("data")) { QVariantMap dataMap = map.value("data").toMap(); qCDebug(dcMiele()) << "key" << dataMap.value("key").toString() << "value" << dataMap.value("value").toString() << dataMap.value("unit").toString(); } else if (map.contains("error")) { qCWarning(dcMiele()) << "Send command" << map.value("error").toMap().value("description").toString(); } emit commandExecuted(commandId, true); }); return commandId; } void Miele::setAuthenticated(bool state) { if (state != m_authenticated) { m_authenticated = state; emit authenticationStatusChanged(state); } } void Miele::setConnected(bool state) { if (state != m_connected) { m_connected = state; emit connectionChanged(state); } } bool Miele::checkStatusCode(QNetworkReply *reply, const QByteArray &rawData) { // Check for the internet connection if (reply->error() == QNetworkReply::NetworkError::HostNotFoundError || reply->error() == QNetworkReply::NetworkError::UnknownNetworkError || reply->error() == QNetworkReply::NetworkError::TemporaryNetworkFailureError) { qCWarning(dcMiele()) << "Connection error" << reply->errorString(); setConnected(false); setAuthenticated(false); return false; } else { setConnected(true); } int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); QJsonParseError error; QJsonDocument jsonDoc = QJsonDocument::fromJson(rawData, &error); switch (status){ case 200: //The request was successful. Typically returned for successful GET requests. case 204: //The request was successful. Typically returned for successful PUT/DELETE requests with no payload. break; case 400: //Error occurred (e.g. validation error - value is out of range) if(!jsonDoc.toVariant().toMap().contains("error")) { if(jsonDoc.toVariant().toMap().value("error").toString() == "invalid_client") { qWarning(dcMiele()) << "Client token provided doesn’t correspond to client that generated auth code."; } if(jsonDoc.toVariant().toMap().value("error").toString() == "invalid_redirect_uri") { qWarning(dcMiele()) << "Missing redirect_uri parameter."; } if(jsonDoc.toVariant().toMap().value("error").toString() == "invalid_code") { qWarning(dcMiele()) << "Expired authorization code."; } } setAuthenticated(false); return false; case 401: qWarning(dcMiele()) << "Client does not have permission to use this API."; setAuthenticated(false); return false; case 403: qCWarning(dcMiele()) << "Forbidden, Scope has not been granted or home appliance is not assigned to HC account"; setAuthenticated(false); return false; case 404: qCWarning(dcMiele()) << "Not Found. This resource is not available (e.g. no images on washing machine)"; return false; case 405: qWarning(dcMiele()) << "Wrong HTTP method used."; setAuthenticated(false); return false; case 408: qCWarning(dcMiele())<< "Request Timeout, API Server failed to produce an answer or has no connection to backend service"; return false; case 409: qCWarning(dcMiele()) << "Conflict - Command/Query cannot be executed for the home appliance, the error response contains the error details"; qCWarning(dcMiele()) << "Error" << jsonDoc.toVariant().toMap().value("error").toString(); return false; case 415: qCWarning(dcMiele())<< "Unsupported Media Type. The request's Content-Type is not supported"; return false; case 429: qCWarning(dcMiele())<< "Too Many Requests, the number of requests for a specific endpoint exceeded the quota of the client"; return false; case 500: qCWarning(dcMiele())<< "Internal Server Error, in case of a server configuration error or any errors in resource files"; return false; case 503: qCWarning(dcMiele())<< "Service Unavailable,if a required backend service is not available"; return false; default: break; } if (error.error != QJsonParseError::NoError) { qCWarning(dcMiele()) << "Received invalide JSON object" << rawData; qCWarning(dcMiele()) << "Status" << status; qCWarning(dcMiele()) << "Error" << error.errorString(); setAuthenticated(false); return false; } setAuthenticated(true); return true; } void Miele::onRefreshTimeout() { if (QDateTime::currentMSecsSinceEpoch() > m_accessTokenExpireTime) { qCDebug(dcMiele()) << "Refresh access token"; getAccessTokenFromRefreshToken(m_refreshToken); } }