POC of fridge thing.

Fixed authentication (refresh token needs to be saved again when
refreshing access token).
This commit is contained in:
Paul Bot 2021-09-08 10:34:28 +03:00
parent ef95857664
commit 7d2de6f268
5 changed files with 252 additions and 21 deletions

View File

@ -30,6 +30,7 @@
#include "integrationpluginmiele.h"
#include "plugininfo.h"
#include <cmath>
#include <QJsonDocument>
#include <QUrl>
@ -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<Miele::DeviceShort> devices)
{
qCDebug(dcMiele()) << "Found " << devices.size() << " devices";
Miele *miele = static_cast<Miele*>(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<Miele*>(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!";
}
}

View File

@ -67,13 +67,18 @@ private:
QHash<ThingClassId, ParamTypeId> m_idParamTypeIds;
QHash<ThingClassId, StateTypeId> m_connectedStateTypeIds;
QHash<QString, ThingClassId> 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<Miele::DeviceShort> devices);
void onDeviceStateReceived(const QString &deviceId, const QVariantMap &deviceState);
};
#endif // INTEGRATIONPLUGINMIELE_H

View File

@ -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"
}
]
},

View File

@ -35,6 +35,7 @@
#include <QNetworkRequest>
#include <QUrl>
#include <QJsonObject>
#include <QJsonArray>
#include <QUrlQuery>
#include <QMetaEnum>
@ -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<DeviceShort> 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;
}

View File

@ -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<DeviceShort> devices);
void deviceStateReceived(const QString &deviceId, const QVariantMap &deviceState);
private slots:
void onRefreshTimeout();