/////////////////////////////////////////////////////////////////////////////////// // Copyright (C) 2022 Jon Beniston, M7RCE // // // // This program 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 as version 3 of the License, or // // (at your option) any later version. // // // // This program 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 V3 for more details. // // // // You should have received a copy of the GNU General Public License // // along with this program. If not, see . // /////////////////////////////////////////////////////////////////////////////////// #include #include #include #include #include #include #include #include "util/iot/tplink.h" #include "util/simpleserializer.h" const QString TPLinkCommon::m_url = "https://wap.tplinkcloud.com"; TPLinkCommon::TPLinkCommon(const QString& username, const QString &password) : m_loggedIn(false), m_outstandingRequest(false), m_username(username), m_password(password), m_networkManager(nullptr) { } void TPLinkCommon::login() { QUrl url(m_url); QNetworkRequest request(url); request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); QJsonObject params { {"appType", "Kasa_Android"}, {"cloudUserName", m_username}, {"cloudPassword", m_password}, {"terminalUUID", "9cc4653e-338f-48e4-b8ca-6ed3f67631e4"} }; QJsonObject object { {"method", "login"}, {"params", params} }; QJsonDocument document; document.setObject(object); m_networkManager->post(request, document.toJson()); } void TPLinkCommon::handleLoginReply(QNetworkReply* reply, QString &errorMessage) { if (reply) { if (!reply->error()) { QJsonDocument document = QJsonDocument::fromJson(reply->readAll()); if (document.isObject()) { //qDebug() << "Received " << document; if (!m_loggedIn) { QJsonObject obj = document.object(); if (obj.contains(QStringLiteral("error_code"))) { int errorCode = obj.value(QStringLiteral("error_code")).toInt(); if (!errorCode) { if (obj.contains(QStringLiteral("result"))) { QJsonObject result = obj.value(QStringLiteral("result")).toObject(); if (result.contains(QStringLiteral("token"))) { m_loggedIn = true; m_token = result.value(QStringLiteral("token")).toString(); } else { qDebug() << "TPLinkDevice::handleReply: Object doesn't contain a token: " << result; } } else { qDebug() << "TPLinkDevice::handleReply: Object doesn't contain a result object: " << obj; } } else { qDebug() << "TPLinkDevice::handleReply: Non-zero error_code while logging in: " << errorCode; if (obj.contains(QStringLiteral("msg"))) { QString msg = obj.value(QStringLiteral("msg")).toString(); qDebug() << "TPLinkDevice::handleReply: Error message: " << msg; // Typical msg is "Incorrect email or password" errorMessage = QString("TP-Link: Failed to log in. %1").arg(msg); } else { errorMessage = QString("TP-Link: Failed to log in. Error code: %1").arg(errorCode); } } } else { qDebug() << "TPLinkDevice::handleReply: Object doesn't contain an error_code: " << obj; } } } else { qDebug() << "TPLinkDevice::handleReply: Document is not an object: " << document; } } else { qDebug() << "TPLinkDevice::handleReply: error: " << reply->error(); } reply->deleteLater(); } else { qDebug() << "TPLinkDevice::handleReply: reply is null"; } if (!m_loggedIn && errorMessage.isEmpty()) { errorMessage = "TP-Link: Failed to log in."; } } TPLinkDevice::TPLinkDevice(const QString& username, const QString &password, const QString &deviceId, DeviceDiscoverer::DeviceInfo *info) : Device(info), TPLinkCommon(username, password), m_deviceId(deviceId) { m_networkManager = new QNetworkAccessManager(); QObject::connect( m_networkManager, &QNetworkAccessManager::finished, this, &TPLinkDevice::handleReply ); login(); } TPLinkDevice::~TPLinkDevice() { QObject::disconnect( m_networkManager, &QNetworkAccessManager::finished, this, &TPLinkDevice::handleReply ); delete m_networkManager; } void TPLinkDevice::getState() { if (!m_loggedIn) { m_outstandingRequest = true; return; } QUrl url(m_url); QNetworkRequest request(url); request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); QJsonObject system; system.insert("get_sysinfo", QJsonValue()); QJsonObject emeter; emeter.insert("get_realtime", QJsonValue()); QJsonObject requestData { {"system", system}, {"emeter", emeter} }; QJsonObject params { {"deviceId", m_deviceId}, {"requestData", requestData}, {"token", m_token} }; QJsonObject object { {"method", "passthrough"}, {"params", params} }; QJsonDocument document; document.setObject(object); m_networkManager->post(request, document.toJson()); } void TPLinkDevice::setState(const QString &controlId, bool state) { if (!m_loggedIn) { // Should we queue these and apply after logged in? qDebug() << "TPLinkDevice::setState: Unable to set state for " << controlId << " to " << state << " as not yet logged in"; return; } QUrl url(m_url); QNetworkRequest request(url); request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); QJsonObject stateObj { {"state", (int)state} }; QJsonObject system { {"set_relay_state", stateObj} }; QJsonObject requestData { {"system", system} }; if (controlId != "switch") { QJsonArray childIds { controlId }; QJsonObject context { {"child_ids", childIds} }; requestData.insert("context", QJsonValue(context)); } QJsonObject params { {"deviceId", m_deviceId}, {"requestData", requestData}, {"token", m_token} }; QJsonObject object { {"method", "passthrough"}, {"params", params} }; QJsonDocument document; document.setObject(object); m_networkManager->post(request, document.toJson()); } void TPLinkDevice::handleReply(QNetworkReply* reply) { if (!m_loggedIn) { QString errorMessage; TPLinkCommon::handleLoginReply(reply, errorMessage); if (!errorMessage.isEmpty()) { emit error(errorMessage); } else if (m_outstandingRequest) { m_outstandingRequest = true; getState(); } } else if (reply) { if (!reply->error()) { QJsonDocument document = QJsonDocument::fromJson(reply->readAll()); if (document.isObject()) { //qDebug() << "Received " << document; QJsonObject obj = document.object(); if (obj.contains(QStringLiteral("result"))) { QJsonObject resultObj = obj.value(QStringLiteral("result")).toObject(); QHash status; if (resultObj.contains(QStringLiteral("responseData"))) { QJsonObject responseDataObj = resultObj.value(QStringLiteral("responseData")).toObject(); if (responseDataObj.contains(QStringLiteral("system"))) { QJsonObject systemObj = responseDataObj.value(QStringLiteral("system")).toObject(); if (systemObj.contains(QStringLiteral("get_sysinfo"))) { QJsonObject sysInfoObj = systemObj.value(QStringLiteral("get_sysinfo")).toObject(); if (sysInfoObj.contains(QStringLiteral("child_num"))) { QJsonArray children = sysInfoObj.value(QStringLiteral("children")).toArray(); for (auto childRef : children) { QJsonObject childObj = childRef.toObject(); if (childObj.contains(QStringLiteral("state")) && childObj.contains(QStringLiteral("id"))) { int state = childObj.value(QStringLiteral("state")).toInt(); QString id = childObj.value(QStringLiteral("id")).toString(); status.insert(id, state); // key should match id in discoverer } } } else if (sysInfoObj.contains(QStringLiteral("relay_state"))) { int state = sysInfoObj.value(QStringLiteral("relay_state")).toInt(); status.insert("switch", state); // key should match id in discoverer } } } // KP115 has emeter, but KP105 doesn't if (responseDataObj.contains(QStringLiteral("emeter"))) { QJsonObject emeterObj = responseDataObj.value(QStringLiteral("emeter")).toObject(); if (emeterObj.contains(QStringLiteral("get_realtime"))) { QJsonObject realtimeObj = emeterObj.value(QStringLiteral("get_realtime")).toObject(); if (realtimeObj.contains(QStringLiteral("current_ma"))) { double current = realtimeObj.value(QStringLiteral("current_ma")).toDouble(); status.insert("current", current / 1000.0); } if (realtimeObj.contains(QStringLiteral("voltage_mv"))) { double voltage = realtimeObj.value(QStringLiteral("voltage_mv")).toDouble(); status.insert("voltage", voltage / 1000.0); } if (realtimeObj.contains(QStringLiteral("power_mw"))) { double power = realtimeObj.value(QStringLiteral("power_mw")).toDouble(); status.insert("power", power / 1000.0); } } } } emit deviceUpdated(status); } else if (obj.contains(QStringLiteral("error_code"))) { // If a device isn't available, we can get: // {"error_code":-20002,"msg":"Request timeout"} // {"error_code":-20571,"msg":"Device is offline"} int errorCode = obj.value(QStringLiteral("error_code")).toInt(); QString msg = obj.value(QStringLiteral("msg")).toString(); qDebug() << "TPLinkDevice::handleReply: Error code: " << errorCode << " " << msg; emit deviceUnavailable(); } else { qDebug() << "TPLinkDevice::handleReply: Object doesn't contain a result or error_code: " << obj; } } else { qDebug() << "TPLinkDevice::handleReply: Document is not an object: " << document; } } else { qDebug() << "TPLinkDevice::handleReply: error: " << reply->error(); } reply->deleteLater(); } else { qDebug() << "TPLinkDevice::handleReply: reply is null"; } } TPLinkDeviceDiscoverer::TPLinkDeviceDiscoverer(const QString& username, const QString &password) : TPLinkCommon(username, password) { m_networkManager = new QNetworkAccessManager(); QObject::connect( m_networkManager, &QNetworkAccessManager::finished, this, &TPLinkDeviceDiscoverer::handleReply ); login(); } TPLinkDeviceDiscoverer::~TPLinkDeviceDiscoverer() { QObject::disconnect( m_networkManager, &QNetworkAccessManager::finished, this, &TPLinkDeviceDiscoverer::handleReply ); delete m_networkManager; } void TPLinkDeviceDiscoverer::getDevices() { if (!m_loggedIn) { m_outstandingRequest = true; return; } QUrl url(m_url); QNetworkRequest request(url); request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); QJsonObject params { {"token", m_token} }; QJsonObject object { {"method", "getDeviceList"}, {"params", params} }; QJsonDocument document; document.setObject(object); m_networkManager->post(request, document.toJson()); } void TPLinkDeviceDiscoverer::getState(const QString &deviceId) { QUrl url(m_url); QNetworkRequest request(url); request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); QJsonObject system; system.insert("get_sysinfo", QJsonValue()); QJsonObject emeter; emeter.insert("get_realtime", QJsonValue()); QJsonObject requestData { {"system", system}, {"emeter", emeter} }; QJsonObject params { {"deviceId", deviceId}, {"requestData", requestData}, {"token", m_token} }; QJsonObject object { {"method", "passthrough"}, {"params", params} }; QJsonDocument document; document.setObject(object); m_getStateReplies.insert(m_networkManager->post(request, document.toJson()), deviceId); } void TPLinkDeviceDiscoverer::handleReply(QNetworkReply* reply) { if (!m_loggedIn) { QString errorMessage; TPLinkCommon::handleLoginReply(reply, errorMessage); if (!errorMessage.isEmpty()) { emit error(errorMessage); } else if (m_outstandingRequest) { m_outstandingRequest = false; getDevices(); } } else if (reply) { if (!reply->error()) { QJsonDocument document = QJsonDocument::fromJson(reply->readAll()); if (document.isObject()) { //qDebug() << "Received " << document; QJsonObject obj = document.object(); if (m_getStateReplies.contains(reply)) { // Reply for getState m_getStateReplies.remove(reply); QJsonObject resultObj = obj.value(QStringLiteral("result")).toObject(); if (resultObj.contains(QStringLiteral("responseData"))) { QJsonObject responseDataObj = resultObj.value(QStringLiteral("responseData")).toObject(); if (responseDataObj.contains(QStringLiteral("system"))) { DeviceInfo info; QJsonObject systemObj = responseDataObj.value(QStringLiteral("system")).toObject(); if (systemObj.contains(QStringLiteral("get_sysinfo"))) { QJsonObject sysInfoObj = systemObj.value(QStringLiteral("get_sysinfo")).toObject(); if (sysInfoObj.contains(QStringLiteral("alias"))) { info.m_name = sysInfoObj.value(QStringLiteral("alias")).toString(); } if (sysInfoObj.contains(QStringLiteral("model"))) { info.m_model = sysInfoObj.value(QStringLiteral("model")).toString(); } if (sysInfoObj.contains(QStringLiteral("deviceId"))) { info.m_id = sysInfoObj.value(QStringLiteral("deviceId")).toString(); } if (sysInfoObj.contains(QStringLiteral("child_num"))) { QJsonArray children = sysInfoObj.value(QStringLiteral("children")).toArray(); int child = 1; for (auto childRef : children) { QJsonObject childObj = childRef.toObject(); ControlInfo *controlInfo = new ControlInfo(); controlInfo->m_id = childObj.value(QStringLiteral("id")).toString(); if (childObj.contains(QStringLiteral("alias"))) { controlInfo->m_name = childObj.value(QStringLiteral("alias")).toString(); } controlInfo->m_type = DeviceDiscoverer::BOOL; info.m_controls.append(controlInfo); child++; } } else if (sysInfoObj.contains(QStringLiteral("relay_state"))) { ControlInfo *controlInfo = new ControlInfo(); controlInfo->m_id = "switch"; if (sysInfoObj.contains(QStringLiteral("alias"))) { controlInfo->m_name = sysInfoObj.value(QStringLiteral("alias")).toString(); } controlInfo->m_type = DeviceDiscoverer::BOOL; info.m_controls.append(controlInfo); } } else { qDebug() << "TPLinkDeviceDiscoverer::handleReply: get_sysinfo missing"; } // KP115 has energy meter, but KP105 doesn't. KP105 will have emeter object, but without get_realtime sub-object if (responseDataObj.contains(QStringLiteral("emeter"))) { QJsonObject emeterObj = responseDataObj.value(QStringLiteral("emeter")).toObject(); if (emeterObj.contains(QStringLiteral("get_realtime"))) { QJsonObject realtimeObj = emeterObj.value(QStringLiteral("get_realtime")).toObject(); if (realtimeObj.contains(QStringLiteral("current_ma"))) { SensorInfo *currentSensorInfo = new SensorInfo(); currentSensorInfo->m_name = "Current"; currentSensorInfo->m_id = "current"; currentSensorInfo->m_type = DeviceDiscoverer::FLOAT; currentSensorInfo->m_units = "A"; info.m_sensors.append(currentSensorInfo); } if (realtimeObj.contains(QStringLiteral("voltage_mv"))) { SensorInfo *voltageSensorInfo = new SensorInfo(); voltageSensorInfo->m_name = "Voltage"; voltageSensorInfo->m_id = "voltage"; voltageSensorInfo->m_type = DeviceDiscoverer::FLOAT; voltageSensorInfo->m_units = "V"; info.m_sensors.append(voltageSensorInfo); } if (realtimeObj.contains(QStringLiteral("power_mw"))) { SensorInfo *powerSensorInfo = new SensorInfo(); powerSensorInfo->m_name = "Power"; powerSensorInfo->m_id = "power"; powerSensorInfo->m_type = DeviceDiscoverer::FLOAT; powerSensorInfo->m_units = "W"; info.m_sensors.append(powerSensorInfo); } } } if (info.m_controls.size() > 0) { m_devices.append(info); } else { qDebug() << "TPLinkDeviceDiscoverer::handleReply: No controls in info"; } } } else { qDebug() << "TPLinkDeviceDiscoverer::handleReply: No responseData"; } if (m_getStateReplies.size() == 0) { emit deviceList(m_devices); m_devices.clear(); } } else { // Reply for getDevice if (obj.contains(QStringLiteral("result"))) { QJsonObject resultObj = obj.value(QStringLiteral("result")).toObject(); if (resultObj.contains(QStringLiteral("deviceList"))) { QJsonArray deviceArray = resultObj.value(QStringLiteral("deviceList")).toArray(); for (auto deviceRef : deviceArray) { QJsonObject deviceObj = deviceRef.toObject(); if (deviceObj.contains(QStringLiteral("deviceId")) && deviceObj.contains(QStringLiteral("deviceType"))) { // In order to discover what controls and sensors a device has, we need to get sysinfo getState(deviceObj.value(QStringLiteral("deviceId")).toString()); } else { qDebug() << "TPLinkDeviceDiscoverer::handleReply: deviceList element doesn't contain a deviceId: " << deviceObj; } } } else { qDebug() << "TPLinkDeviceDiscoverer::handleReply: result doesn't contain a deviceList: " << resultObj; } } else { qDebug() << "TPLinkDeviceDiscoverer::handleReply: Object doesn't contain a result: " << obj; } } } else { qDebug() << "TPLinkDeviceDiscoverer::handleReply: Document is not an object: " << document; } } else { qDebug() << "TPLinkDeviceDiscoverer::handleReply: error: " << reply->error(); } reply->deleteLater(); } else { qDebug() << "TPLinkDeviceDiscoverer::handleReply: reply is null"; } }