From 7d2de6f26894f194c81900eee66a2baea0fc4509 Mon Sep 17 00:00:00 2001 From: Paul Bot Date: Wed, 8 Sep 2021 10:34:28 +0300 Subject: [PATCH] POC of fridge thing. Fixed authentication (refresh token needs to be saved again when refreshing access token). --- miele/integrationpluginmiele.cpp | 124 +++++++++++++++++++++++++++--- miele/integrationpluginmiele.h | 5 ++ miele/integrationpluginmiele.json | 38 ++++++++- miele/miele.cpp | 94 +++++++++++++++++++--- miele/miele.h | 12 +++ 5 files changed, 252 insertions(+), 21 deletions(-) diff --git a/miele/integrationpluginmiele.cpp b/miele/integrationpluginmiele.cpp index d2adbac9..b1dc8ec0 100644 --- a/miele/integrationpluginmiele.cpp +++ b/miele/integrationpluginmiele.cpp @@ -30,6 +30,7 @@ #include "integrationpluginmiele.h" #include "plugininfo.h" +#include #include #include @@ -56,6 +57,14 @@ IntegrationPluginMiele::IntegrationPluginMiele() m_connectedStateTypeIds.insert(cookTopThingClassId, cookTopConnectedStateTypeId); m_connectedStateTypeIds.insert(cleaningRobotThingClassId, cleaningRobotConnectedStateTypeId); m_connectedStateTypeIds.insert(hoodThingClassId, hoodConnectedStateTypeId); + + m_mieleDeviceTypeLabelToThingClassId.insert("oven", ovenThingClassId); + m_mieleDeviceTypeLabelToThingClassId.insert("washing machine", washerThingClassId); + m_mieleDeviceTypeLabelToThingClassId.insert("dish washer", dishwasherThingClassId); + m_mieleDeviceTypeLabelToThingClassId.insert("coffee system", coffeeMakerThingClassId); + m_mieleDeviceTypeLabelToThingClassId.insert("tumber dryer", dryerThingClassId); + m_mieleDeviceTypeLabelToThingClassId.insert("refrigerator", fridgeThingClassId); + } void IntegrationPluginMiele::startPairing(ThingPairingInfo *info) @@ -148,7 +157,15 @@ void IntegrationPluginMiele::setupThing(ThingSetupInfo *info) m_mieleConnections.insert(info->thing(), miele); info->finish(Thing::ThingErrorNoError); }); + connect(miele, &Miele::receivedRefreshToken, info, [info, thing, this](const QByteArray &refreshToken){ + pluginStorage()->beginGroup(thing->id().toString()); + pluginStorage()->setValue("refresh_token", refreshToken); + pluginStorage()->endGroup(); + info->finish(Thing::ThingErrorNoError); + }); connect(info, &ThingSetupInfo::aborted, miele, &Miele::deleteLater); + connect(miele, &Miele::devicesFound, this, &IntegrationPluginMiele::onDevicesFound); + connect(miele, &Miele::deviceStateReceived, this, &IntegrationPluginMiele::onDeviceStateReceived); } } else if (m_idParamTypeIds.contains(thing->thingClassId())) { Thing *parentThing = myThings().findById(thing->parentId()); @@ -172,7 +189,7 @@ void IntegrationPluginMiele::postSetupThing(Thing *thing) qCDebug(dcMiele()) << "Post setup thing" << thing->name(); if (!m_pluginTimer15min) { qCDebug(dcMiele()) << "Registering plugin timer"; - m_pluginTimer15min = hardwareManager()->pluginTimerManager()->registerTimer(60*15); + m_pluginTimer15min = hardwareManager()->pluginTimerManager()->registerTimer(60 * 1); connect(m_pluginTimer15min, &PluginTimer::timeout, this, [this]() { qCDebug(dcMiele()) << "Plugin timer triggered"; Q_FOREACH (Thing *thing, myThings().filterByThingClassId(mieleAccountThingClassId)) { @@ -181,10 +198,10 @@ void IntegrationPluginMiele::postSetupThing(Thing *thing) qWarning(dcMiele()) << "No Miele account found for" << thing->name(); continue; } - miele->getDevices(); + miele->getDevicesShort(); Q_FOREACH (Thing *childThing, myThings().filterByParentId(thing->id())) { - QString deviceId = childThing->paramValue(m_idParamTypeIds.value(childThing->thingClassId())).toString(); - miele->getDevice(deviceId); + QString deviceId = childThing->paramValue(m_idParamTypeIds.value(childThing->thingClassId())).toString(); + miele->getDeviceState(deviceId); } } }); @@ -192,7 +209,7 @@ void IntegrationPluginMiele::postSetupThing(Thing *thing) if (thing->thingClassId() == mieleAccountThingClassId) { Miele *miele = m_mieleConnections.value(thing); - miele->getDevices(); + miele->getDevicesShort(); //miele->connectEventStream(); thing->setStateValue(mieleAccountConnectedStateTypeId, true); thing->setStateValue(mieleAccountLoggedInStateTypeId, true); @@ -206,7 +223,7 @@ void IntegrationPluginMiele::postSetupThing(Thing *thing) if (!miele) { qCWarning(dcMiele()) << "Could not find HomeConnect connection for thing" << thing->name(); } else { - miele->getDevice(deviceId); + miele->getDeviceState(deviceId); } } else { Q_ASSERT_X(false, "postSetupThing", QString("Unhandled thingClassId: %1").arg(thing->thingClassId().toString()).toUtf8()); @@ -221,10 +238,17 @@ void IntegrationPluginMiele::executeAction(ThingActionInfo *info) if (!miele) { return info->finish(Thing::ThingErrorHardwareNotAvailable); } - QString haid = thing->paramValue(m_idParamTypeIds.value(thing->thingClassId())).toString(); - - if (thing->thingClassId() == ovenThingClassId) { + QString deviceId = thing->paramValue(m_idParamTypeIds.value(thing->thingClassId())).toString(); + if (thing->thingClassId() == fridgeThingClassId) { + if (action.actionTypeId() == fridgeTargetTemperatureActionTypeId) { + double targetTemp = action.param(fridgeTargetTemperatureActionTargetTemperatureParamTypeId).value().toDouble(); + int iTargetTemp = floor(targetTemp); + qCDebug(dcMiele()) << "Setting fridge temp: " << iTargetTemp; + miele->setTargetTemperature(deviceId, 1, iTargetTemp); + info->finish(Thing::ThingErrorNoError); + thing->setStateValue(fridgeTargetTemperatureStateTypeId, iTargetTemp); + } } else if (thing->thingClassId() == ovenThingClassId) { } else { @@ -327,4 +351,86 @@ void IntegrationPluginMiele::onRequestExecuted(QUuid requestId, bool success) } } +void IntegrationPluginMiele::onDevicesFound(QList devices) +{ + qCDebug(dcMiele()) << "Found " << devices.size() << " devices"; + Miele *miele = static_cast(sender()); + if(m_mieleConnections.values().contains(miele)) { + Thing *parentDevice = myThings().findById(m_mieleConnections.key(miele)->id()); + qCDebug(dcMiele()) << devices.size() << " devices received for prent: " << parentDevice->id(); + + ThingDescriptors descriptors; + foreach(Miele::DeviceShort ds, devices) { + qCDebug(dcMiele()) << "Setting up device: " << ds.type << " " << ds.name << " " << ds.state; + if (!m_mieleDeviceTypeLabelToThingClassId.contains(ds.type.toLower())) { + qCDebug(dcMiele()) << "Device type [" << ds.type << "] is not supported!"; + continue; + } + ThingClassId deviceClassId = m_mieleDeviceTypeLabelToThingClassId.value(ds.type.toLower()); + ThingDescriptor descriptor(deviceClassId, ds.name, "Appliance type: " + ds.type, parentDevice->id()); + ParamList params; + params.append(Param(m_idParamTypeIds.value(deviceClassId), ds.fabNumber)); + if (myThings().findByParams(params)) { + continue; + } + descriptor.setParams(params); + descriptors.append(descriptor); + } + emit autoThingsAppeared(descriptors); + + } else { + // TODO: improve debug message + qCWarning(dcMiele()) << "Miele connection not linked to a thingId"; + } +} + + +void IntegrationPluginMiele::onDeviceStateReceived(const QString &deviceId, const QVariantMap &deviceState) +{ + Miele *miele = static_cast(sender()); + if (!m_mieleConnections.values().contains(miele)) { + qCWarning(dcMiele()) << "Can't find Miele connection!"; + return; + } + + Thing *parentDevice = myThings().findById(m_mieleConnections.key(miele)->id()); + if (!parentDevice) { + qCWarning(dcMiele()) << "Can't find parent thing!"; + return; + } + + foreach(Thing *thing, myThings().filterByParentId(parentDevice->id())) { + if (thing->paramValue(m_idParamTypeIds.value(thing->thingClassId())).toString() == deviceId) { + qCDebug(dcMiele()) << "Received state for: " << thing->name(); + setThingState(thing, deviceState); + break; + } + } + +} + +void IntegrationPluginMiele::setThingState(Thing *thing, const QVariantMap &state) +{ + qCDebug(dcMiele()) << "Setting device state: " << thing->name(); + if (thing->thingClassId() == fridgeThingClassId) { + qCDebug(dcMiele()) << "Setting state for fridge!"; + + bool connected = state.value("status").toMap().value("value_raw").toInt() == Miele::StatusNotConnected + ? false + : true; + thing->setStateValue(m_connectedStateTypeIds.value(thing->thingClassId()), connected); + + // Fridges should have only one temperature zone + QVariant fridgeTempZone = state.value("temperature").toList().first(); + double fridgeTemp = fridgeTempZone.toMap().value("value_raw").toDouble(); + thing->setStateValue(fridgeTemperatureStateTypeId, fridgeTemp / 100); + + QVariant fridgeTargetTempZone = state.value("targetTemperature").toList().first(); + double fridgeTargetTemp = fridgeTargetTempZone.toMap().value("value_raw").toDouble(); + thing->setStateValue(fridgeTargetTemperatureStateTypeId, fridgeTargetTemp / 100); + + } else { + qCWarning(dcMiele()) << "Device " << thing->name() << " is not yet supported!"; + } +} diff --git a/miele/integrationpluginmiele.h b/miele/integrationpluginmiele.h index 87999748..03449e9b 100644 --- a/miele/integrationpluginmiele.h +++ b/miele/integrationpluginmiele.h @@ -67,13 +67,18 @@ private: QHash m_idParamTypeIds; QHash m_connectedStateTypeIds; + QHash m_mieleDeviceTypeLabelToThingClassId; Miele *createMieleConnection(); + void setThingState(Thing *thing, const QVariantMap &state); private slots: void onConnectionChanged(bool connected); void onAuthenticationStatusChanged(bool authenticated); void onRequestExecuted(QUuid requestId, bool success); + + void onDevicesFound(QList devices); + void onDeviceStateReceived(const QString &deviceId, const QVariantMap &deviceState); }; #endif // INTEGRATIONPLUGINMIELE_H diff --git a/miele/integrationpluginmiele.json b/miele/integrationpluginmiele.json index 1841a3d3..43cb6e0b 100644 --- a/miele/integrationpluginmiele.json +++ b/miele/integrationpluginmiele.json @@ -178,7 +178,7 @@ "id": "a03e17fd-8da8-4977-8ac3-c058ada9f547", "name": "fridge", "displayName": "Fridge Freezer", - "interfaces": ["connectable"], + "interfaces": ["thermostat", "connectable"], "createMethods": ["auto"], "paramTypes": [ { @@ -198,6 +198,42 @@ "defaultValue": true, "cached": false, "type": "bool" + }, + { + "id": "ef656321-e2c8-4339-be18-aa3cdb53dea6", + "name": "temperature", + "displayName": "Temperature", + "displayNameEvent": "Temperature changed", + "defaultValue": 0, + "type": "double", + "minValue": "0", + "maxValue": "10", + "unit": "DegreeCelsius" + }, + { + "id": "ebac99f4-cb89-4813-9b21-a9c6bf5209d5", + "name": "targetTemperature", + "displayName": "Temperature Setting", + "displayNameEvent": "Temperature setting changed", + "displayNameAction": "Set target temperature", + "defaultValue": 0, + "type": "double", + "minValue": "0", + "maxValue": "10", + "unit": "DegreeCelsius", + "writable": true, + "ioType": "analogInput" + }, + { + "id": "60a46368-febc-4cff-bbfb-99064dbb93a9", + "name": "power", + "displayName": "On/off", + "displayNameEvent": "Turned on/off", + "displayNameAction": "Turn on/off", + "type": "bool", + "defaultValue": false, + "writable": true, + "ioType": "digitalInput" } ] }, diff --git a/miele/miele.cpp b/miele/miele.cpp index 694287d4..633d7312 100644 --- a/miele/miele.cpp +++ b/miele/miele.cpp @@ -35,6 +35,7 @@ #include #include #include +#include #include #include @@ -93,6 +94,10 @@ void Miele::getAccessTokenFromRefreshToken(const QByteArray &refreshToken) return; } + //delete me + qCDebug(dcMiele()) << "Refresh token is not empty: " << refreshToken; + qCDebug(dcMiele()) << "Client: " << m_clientId << " secret: " << m_clientSecret; + QUrl url(m_tokenUrl); QUrlQuery query; query.clear(); @@ -106,7 +111,7 @@ void Miele::getAccessTokenFromRefreshToken(const QByteArray &refreshToken) QNetworkReply *reply = m_networkManager->post(request, query.toString().toUtf8()); connect(reply, &QNetworkReply::finished, reply, &QNetworkReply::deleteLater); - connect(reply, &QNetworkReply::finished, this, [this, reply](){ + connect(reply, &QNetworkReply::finished, this, [this, reply, refreshToken](){ QByteArray rawData = reply->readAll(); if (!checkStatusCode(reply, rawData)) { @@ -114,13 +119,16 @@ void Miele::getAccessTokenFromRefreshToken(const QByteArray &refreshToken) } QJsonDocument data = QJsonDocument::fromJson(rawData); - if(!data.toVariant().toMap().contains("access_token")) { + 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(); @@ -145,7 +153,7 @@ void Miele::getAccessTokenFromAuthorizationCode(const QByteArray &authorizationC query.clear(); query.addQueryItem("client_id", m_clientId); query.addQueryItem("client_secret", m_clientSecret); - //query.addQueryItem("vg", "de-DE"); + query.addQueryItem("vg", "de-DE"); query.addQueryItem("redirect_uri", m_redirectUri); query.addQueryItem("grant_type", "authorization_code"); query.addQueryItem("code", authorizationCode); @@ -169,7 +177,7 @@ void Miele::getAccessTokenFromAuthorizationCode(const QByteArray &authorizationC } m_accessToken = jsonDoc.toVariant().toMap().value("access_token").toByteArray(); receivedAccessToken(m_accessToken); - m_refreshToken = jsonDoc.toVariant().toMap().value("refresh_token").toByteArray(); + m_refreshToken = jsonDoc.toVariant().toMap().value("refresh_token").toByteArray(); receivedRefreshToken(m_refreshToken); if (jsonDoc.toVariant().toMap().contains("expires_in")) { @@ -180,7 +188,7 @@ void Miele::getAccessTokenFromAuthorizationCode(const QByteArray &authorizationC qCWarning(dcMiele()) << "Expire time too short"; return; } - m_accessTokenExpireTime = QDateTime::currentMSecsSinceEpoch() + (expireTime-(m_refreshInterval*1000)); + m_accessTokenExpireTime = QDateTime::currentMSecsSinceEpoch() + (expireTime-(m_refreshInterval*1000)); } }); } @@ -207,8 +215,7 @@ void Miele::getDevices() if (!checkStatusCode(reply, rawData)) { return; } - QVariantMap map = QJsonDocument::fromJson(rawData).toVariant().toMap(); - qCDebug(dcMiele()) << "Get devices" << map; + 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(); @@ -218,11 +225,48 @@ void Miele::getDevices() }); } +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.setPath("/v1/devices/" + deviceId); url.setQuery("language=en"); QNetworkRequest request(url); @@ -238,7 +282,31 @@ void Miele::getDevice(const QString &deviceId) return; } QVariantMap map = QJsonDocument::fromJson(rawData).toVariant().toMap(); - qCDebug(dcMiele()) << "Get device" << map; + 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)) { + return; + } + QVariantMap map = QJsonDocument::fromJson(rawData).toVariant().toMap(); + qCDebug(dcMiele()) << "Got device state for: " << deviceId; + emit deviceStateReceived(deviceId, map); }); } @@ -298,11 +366,14 @@ QUuid Miele::setTargetTemperature(const QString &deviceId, int zone, int targetT { QJsonDocument doc; QJsonObject object; + QJsonObject temperatureObj; temperatureObj.insert("zone", zone); temperatureObj.insert("value", targetTemperature); - object.insert("targetTemperature", temperatureObj); - doc.setObject(object); + QJsonArray tempZones; + tempZones.push_front(temperatureObj); + object.insert("targetTemperature", tempZones); + doc.setObject(object); return putAction(deviceId, doc); } @@ -491,6 +562,7 @@ bool Miele::checkStatusCode(QNetworkReply *reply, const QByteArray &rawData) 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; } diff --git a/miele/miele.h b/miele/miele.h index 010e7029..603609ef 100644 --- a/miele/miele.h +++ b/miele/miele.h @@ -90,6 +90,14 @@ public: StatusNotConnected = 255 }; + struct DeviceShort { + QString details; + QString name; + QString fabNumber; + QString state; + QString type; + }; + Miele(NetworkAccessManager *networkmanager, const QByteArray &clientId, const QByteArray &clientSecret, const QString &language = "en", QObject *parent = nullptr); QByteArray accessToken(); QByteArray refreshToken(); @@ -101,7 +109,9 @@ public: // INFORMATION void getDevices(); + void getDevicesShort(); void getDevice(const QString &deviceId); + void getDeviceState(const QString &deviceId); // ACTION void getActions(const QString &deviceId); //The GET action is used to request a device to send the currently supported actions @@ -150,6 +160,8 @@ signals: void receivedRefreshToken(const QByteArray &refreshToken); void receivedAccessToken(const QByteArray &accessToken); void commandExecuted(const QUuid &commandId, bool success); + void devicesFound(QList devices); + void deviceStateReceived(const QString &deviceId, const QVariantMap &deviceState); private slots: void onRefreshTimeout();