diff --git a/homeconnect/homeconnect.cpp b/homeconnect/homeconnect.cpp index 69eef3a9..5822f62f 100644 --- a/homeconnect/homeconnect.cpp +++ b/homeconnect/homeconnect.cpp @@ -49,6 +49,16 @@ HomeConnect::HomeConnect(NetworkAccessManager *networkmanager, const QByteArray } } +QByteArray HomeConnect::accessToken() +{ + return m_accessToken; +} + +QByteArray HomeConnect::refreshToken() +{ + return m_refreshToken; +} + QUrl HomeConnect::getLoginUrl(const QUrl &redirectUrl, const QString &scope) { if (m_clientKey.isEmpty()) { @@ -66,11 +76,12 @@ QUrl HomeConnect::getLoginUrl(const QUrl &redirectUrl, const QString &scope) queryParams.addQueryItem("client_id", m_clientKey); queryParams.addQueryItem("redirect_uri", m_redirectUri); queryParams.addQueryItem("response_type", "code"); - queryParams.addQueryItem("scope", "TODO"); + queryParams.addQueryItem("scope", scope); queryParams.addQueryItem("state", QUuid::createUuid().toString()); queryParams.addQueryItem("nonce", QUuid::createUuid().toString()); - //queryParams.addQueryItem("code_challenge", QUuid::createUuid().toString()); - //queryParams.addQueryItem("code_challenge_method", QUuid::createUuid().toString()); + m_codeChallenge = QUuid::createUuid().toString().remove('{').remove('}').remove("-"); + queryParams.addQueryItem("code_challenge", m_codeChallenge); + queryParams.addQueryItem("code_challenge_method", "plain"); url.setQuery(queryParams); return url; @@ -133,8 +144,8 @@ void HomeConnect::getAccessTokenFromRefreshToken(const QByteArray &refreshToken) request.setRawHeader("Authorization", QString("Basic %1").arg(QString(auth)).toUtf8()); QNetworkReply *reply = m_networkManager->post(request, QByteArray()); + connect(reply, &QNetworkReply::finished, reply, &QNetworkReply::deleteLater); connect(reply, &QNetworkReply::finished, this, [this, reply](){ - reply->deleteLater(); QJsonDocument jsonDoc = QJsonDocument::fromJson(reply->readAll()); int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); @@ -168,32 +179,77 @@ void HomeConnect::getAccessTokenFromAuthorizationCode(const QByteArray &authoriz { // Obtaining access token if(authorizationCode.isEmpty()) - qWarning(dcHomeConnect) << "No auhtorization code given!"; + qWarning(dcHomeConnect) << "No authorization code given!"; if(m_clientKey.isEmpty()) qWarning(dcHomeConnect) << "Client key not set!"; if(m_clientSecret.isEmpty()) qWarning(dcHomeConnect) << "Client secret not set!"; - QUrl url = QUrl(m_baseAuthorizationUrl); - QUrlQuery query; + QUrl url = QUrl(m_baseTokenUrl); + QUrlQuery query; url.setQuery(query); + query.clear(); + query.addQueryItem("client_id", m_clientKey); + query.addQueryItem("client_secret", m_clientSecret); + query.addQueryItem("redirect_uri", m_redirectUri); query.addQueryItem("grant_type", "authorization_code"); query.addQueryItem("code", authorizationCode); - query.addQueryItem("redirect_uri", m_redirectUri); - url.setQuery(query); - + query.addQueryItem("code_verifier", m_codeChallenge); + /* Code verifier which was used during code challenge creation on client side. Please note that it is required if the code_challenge parameter was included in the authorization request. */ QNetworkRequest request(url); - request.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded;charset=utf-8"); + request.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded"); + qCDebug(dcHomeConnect()) << "Get access token" << url.toString(); - QByteArray auth = QByteArray(m_clientKey + ':' + m_clientSecret).toBase64(QByteArray::Base64Encoding | QByteArray::KeepTrailingEquals); - request.setRawHeader("Authorization", QString("Basic %1").arg(QString(auth)).toUtf8()); - - QNetworkReply *reply = m_networkManager->post(request, QByteArray()); + QNetworkReply *reply = m_networkManager->post(request, query.toString().toUtf8()); + connect(reply, &QNetworkReply::finished, reply, &QNetworkReply::deleteLater); connect(reply, &QNetworkReply::finished, this, [this, reply](){ - reply->deleteLater(); - QJsonDocument jsonDoc = QJsonDocument::fromJson(reply->readAll()); - int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); + if (status != 200 || reply->error() != QNetworkReply::NoError) { + qWarning(dcHomeConnect()) << reply->errorString() << status << reply->readAll(); + emit authenticationStatusChanged(false); + return; + } + QJsonDocument jsonDoc = QJsonDocument::fromJson(reply->readAll()); + + qCDebug(dcHomeConnect()) << "HomeConnect accessToken reply:" << this << reply->error() << reply->errorString() << jsonDoc.toJson(); + if(!jsonDoc.toVariant().toMap().contains("access_token") || !jsonDoc.toVariant().toMap().contains("refresh_token") ) { + emit authenticationStatusChanged(false); + return; + } + qCDebug(dcHomeConnect()) << "Access token:" << jsonDoc.toVariant().toMap().value("access_token").toString(); + m_accessToken = jsonDoc.toVariant().toMap().value("access_token").toByteArray(); + + qCDebug(dcHomeConnect()) << "Refresh token:" << jsonDoc.toVariant().toMap().value("refresh_token").toString(); + m_refreshToken = jsonDoc.toVariant().toMap().value("refresh_token").toByteArray(); + + if (jsonDoc.toVariant().toMap().contains("expires_in")) { + int expireTime = jsonDoc.toVariant().toMap().value("expires_in").toInt(); + qCDebug(dcHomeConnect()) << "expires at" << QDateTime::currentDateTime().addSecs(expireTime).toString(); + if (!m_tokenRefreshTimer) { + qWarning(dcHomeConnect()) << "Token refresh timer not initialized"; + emit authenticationStatusChanged(false); + return; + } + m_tokenRefreshTimer->start((expireTime - 20) * 1000); + } + emit authenticationStatusChanged(true); + }); +} + +void HomeConnect::getHomeAppliances() +{ + QUrl url = QUrl(m_baseAuthorizationUrl); + + QNetworkRequest request(url); + + QByteArray auth = QByteArray(m_clientKey + ':' + m_clientSecret).toBase64(QByteArray::Base64Encoding | QByteArray::KeepTrailingEquals); + request.setRawHeader("Authorization", QString("Basic %1").arg(QString(auth)).toUtf8()); + request.setRawHeader("accept", "application/vnd.bsh.sdk.v1+json"); + + QNetworkReply *reply = m_networkManager->get(request); + connect(reply, &QNetworkReply::finished, this, [this, reply](){ + reply->deleteLater(); + QJsonDocument jsonDoc = QJsonDocument::fromJson(reply->readAll()); qCDebug(dcHomeConnect()) << "HomeConnect accessToken reply:" << this << reply->error() << reply->errorString() << jsonDoc.toJson(); if(!jsonDoc.toVariant().toMap().contains("access_token") || !jsonDoc.toVariant().toMap().contains("refresh_token") ) { diff --git a/homeconnect/homeconnect.h b/homeconnect/homeconnect.h index b670ed7a..fd3e3922 100644 --- a/homeconnect/homeconnect.h +++ b/homeconnect/homeconnect.h @@ -40,13 +40,29 @@ class HomeConnect : public QObject { Q_OBJECT public: + enum Type { + + }; + + struct HomeAppliance { + QString name; + QString brand; + QString typeCode; + bool connected; + QString type; + }; HomeConnect(NetworkAccessManager *networkmanager, const QByteArray &clientKey, const QByteArray &clientSecret, QObject *parent = nullptr); + QByteArray accessToken(); + QByteArray refreshToken(); QUrl getLoginUrl(const QUrl &redirectUrl, const QString &scope); void checkStatusCode(int status, const QByteArray &payload); void getAccessTokenFromRefreshToken(const QByteArray &refreshToken); void getAccessTokenFromAuthorizationCode(const QByteArray &authorizationCode); + void getHomeAppliances(); + void getHomeAppliance(const QString &haid); + private: QByteArray m_baseAuthorizationUrl = "https://api.home-connect.com/security/oauth/authorize"; QByteArray m_baseTokenUrl = "https://api.home-connect.com/security/oauth/token"; @@ -56,7 +72,8 @@ private: QByteArray m_accessToken; QByteArray m_refreshToken; - QByteArray m_redirectUri; + QByteArray m_redirectUri = "https://127.0.0.1:8888"; + QString m_codeChallenge; NetworkAccessManager *m_networkManager = nullptr; QTimer *m_tokenRefreshTimer = nullptr; @@ -68,5 +85,7 @@ signals: void connectionChanged(bool connected); void authenticationStatusChanged(bool authenticated); void actionExecuted(QUuid actionId,bool success); + + void receivedHomeAppliances(const QList &appliances); }; #endif // HOMECONNECT_H diff --git a/homeconnect/homeconnect.pro b/homeconnect/homeconnect.pro index 1dde3bbb..8091f613 100644 --- a/homeconnect/homeconnect.pro +++ b/homeconnect/homeconnect.pro @@ -2,8 +2,6 @@ include(../plugins.pri) QT += network -TARGET = $$qtLibraryTarget(nymea_integrationpluginhomeconnect) - SOURCES += \ integrationpluginhomeconnect.cpp \ homeconnect.cpp \ diff --git a/homeconnect/integrationpluginhomeconnect.cpp b/homeconnect/integrationpluginhomeconnect.cpp index 6259d35d..2e7b0bbb 100644 --- a/homeconnect/integrationpluginhomeconnect.cpp +++ b/homeconnect/integrationpluginhomeconnect.cpp @@ -60,14 +60,13 @@ void IntegrationPluginHomeConnect::startPairing(ThingPairingInfo *info) { if (info->thingClassId() == homeConnectConnectionThingClassId) { - HomeConnect *homeConnect = new HomeConnect(hardwareManager()->networkManager(), "TODO", "TODO", this); - QUrl url = homeConnect->getLoginUrl(QUrl("https://127.0.0.1:8888"), "TODO Scope"); + HomeConnect *homeConnect = new HomeConnect(hardwareManager()->networkManager(), "423713AB3EDA5B44BCE6E7B3546C43DADCB27A156C681E30455250637B2213DB", "AE182EA9F1CB99416DFD62CE61BF6DCDB3BB7D4697B58D4499D3792EC9F7412D", this); + QUrl url = homeConnect->getLoginUrl(QUrl("https://127.0.0.1:8888"), "Monitor"); qCDebug(dcHomeConnect()) << "HomeConnect url:" << url; info->setOAuthUrl(url); info->finish(Thing::ThingErrorNoError); m_setupHomeConnectConnections.insert(info->thingId(), homeConnect); } else { - qCWarning(dcHomeConnect()) << "Unhandled pairing metod!"; info->finish(Thing::ThingErrorCreationMethodNotSupported); } @@ -82,43 +81,39 @@ void IntegrationPluginHomeConnect::confirmPairing(ThingPairingInfo *info, const QUrl url(secret); QUrlQuery query(url); QByteArray authorizationCode = query.queryItemValue("code").toLocal8Bit(); - QByteArray state = query.queryItemValue("state").toLocal8Bit(); + //QByteArray state = query.queryItemValue("state").toLocal8Bit(); //TODO evaluate state if it equals the given state - HomeConnect *HomeConnect = m_setupHomeConnectConnections.value(info->thingId()); - - if (!HomeConnect) { + HomeConnect *homeConnect = m_setupHomeConnectConnections.value(info->thingId()); + if (!homeConnect) { qWarning(dcHomeConnect()) << "No HomeConnect connection found for device:" << info->thingName(); m_setupHomeConnectConnections.remove(info->thingId()); - HomeConnect->deleteLater(); + homeConnect->deleteLater(); info->finish(Thing::ThingErrorHardwareFailure); return; } - HomeConnect->getAccessTokenFromAuthorizationCode(authorizationCode); - connect(HomeConnect, &HomeConnect::authenticationStatusChanged, this, [info, this](bool authenticated){ + qCDebug(dcHomeConnect()) << "Authorization code" << authorizationCode; + homeConnect->getAccessTokenFromAuthorizationCode(authorizationCode); + connect(homeConnect, &HomeConnect::authenticationStatusChanged, this, [info, this](bool authenticated){ HomeConnect *homeConnect = static_cast(sender()); - DevicePairingInfo info(devicePairingInfo); + if(!authenticated) { - qWarning(dcHomeConnect()) << "Authentication process failed" << devicePairingInfo.deviceName(); - m_setupHomeConnectConnections.remove(info.deviceId()); + qWarning(dcHomeConnect()) << "Authentication process failed"; + m_setupHomeConnectConnections.remove(info->thingId()); homeConnect->deleteLater(); - info.setStatus(Device::DeviceErrorSetupFailed); - emit pairingFinished(info); + info->finish(Thing::ThingErrorSetupFailed); return; } QByteArray accessToken = homeConnect->accessToken(); QByteArray refreshToken = homeConnect->refreshToken(); qCDebug(dcHomeConnect()) << "Token:" << accessToken << refreshToken; - pluginStorage()->beginGroup(info.deviceId().toString()); + pluginStorage()->beginGroup(info->thingId().toString()); pluginStorage()->setValue("refresh_token", refreshToken); pluginStorage()->endGroup(); - info.setStatus(Device::DeviceErrorNoError); - emit pairingFinished(info); + info->finish(Thing::ThingErrorNoError); }); - devicePairingInfo.setStatus(Device::DeviceErrorAsync); - } else { qCWarning(dcHomeConnect()) << "Invalid thingClassId -> no pairing possible with this device"; info->finish(Thing::ThingErrorThingClassNotFound); @@ -127,6 +122,40 @@ void IntegrationPluginHomeConnect::confirmPairing(ThingPairingInfo *info, const void IntegrationPluginHomeConnect::setupThing(ThingSetupInfo *info) +{ + Thing *thing = info->thing(); + + if (info->thing()->thingClassId() == homeConnectConnectionThingClassId) { + HomeConnect *homeConnect; + if (m_setupHomeConnectConnections.keys().contains(thing->id())) { + //Fresh device setup, has already a fresh access token + qCDebug(dcHomeConnect()) << "HomeConnect OAuth setup complete"; + homeConnect = m_setupHomeConnectConnections.take(thing->id()); + connect(homeConnect, &HomeConnect::connectionChanged, this, &IntegrationPluginHomeConnect::onConnectionChanged); + connect(homeConnect, &HomeConnect::actionExecuted, this, &IntegrationPluginHomeConnect::onRequestExecuted); + connect(homeConnect, &HomeConnect::authenticationStatusChanged, this, &IntegrationPluginHomeConnect::onAuthenticationStatusChanged); + m_homeConnectConnections.insert(thing, homeConnect); + return; + } else { + //device loaded from the device database, needs a new access token; + pluginStorage()->beginGroup(thing->id().toString()); + QByteArray refreshToken = pluginStorage()->value("refresh_token").toByteArray(); + pluginStorage()->endGroup(); + + homeConnect = new HomeConnect(hardwareManager()->networkManager(), "423713AB3EDA5B44BCE6E7B3546C43DADCB27A156C681E30455250637B2213DB", "AE182EA9F1CB99416DFD62CE61BF6DCDB3BB7D4697B58D4499D3792EC9F7412D", this); + connect(homeConnect, &HomeConnect::connectionChanged, this, &IntegrationPluginHomeConnect::onConnectionChanged); + connect(homeConnect, &HomeConnect::actionExecuted, this, &IntegrationPluginHomeConnect::onRequestExecuted); + connect(homeConnect, &HomeConnect::authenticationStatusChanged, this, &IntegrationPluginHomeConnect::onAuthenticationStatusChanged); + homeConnect->getAccessTokenFromRefreshToken(refreshToken); + m_homeConnectConnections.insert(thing, homeConnect); + info->finish(Thing::ThingErrorNoError); + } + } else { + info->finish(Thing::ThingErrorThingClassNotFound); + } +} + +void IntegrationPluginHomeConnect::postSetupThing(Thing *thing) { if (!m_pluginTimer5sec) { m_pluginTimer5sec = hardwareManager()->pluginTimerManager()->registerTimer(5); @@ -156,38 +185,6 @@ void IntegrationPluginHomeConnect::setupThing(ThingSetupInfo *info) }); } - if (info->thing()->thingClassId() == homeConnectConnectionThingClassId) { - HomeConnect *homeConnect; - if (m_setupHomeConnectConnections.keys().contains(thing->id())) { - //Fresh device setup, has already a fresh access token - qCDebug(dcHomeConnect()) << "HomeConnect OAuth setup complete"; - HomeConnect = m_setupHomeConnectConnections.take(device->id()); - connect(homeConnect, &HomeConnect::connectionChanged, this, &IntegrationPluginHomeConnect::onConnectionChanged); - connect(homeConnect, &HomeConnect::actionExecuted, this, &IntegrationPluginHomeConnect::onActionExecuted); - connect(homeConnect, &HomeConnect::authenticationStatusChanged, this, &IntegrationPluginHomeConnect::onAuthenticationStatusChanged); - m_homeConnectConnections.insert(thing, homeConnect); - return; - } else { - //device loaded from the device database, needs a new access token; - pluginStorage()->beginGroup(thing->id().toString()); - QByteArray refreshToken = pluginStorage()->value("refresh_token").toByteArray(); - pluginStorage()->endGroup(); - - homeConnect = new HomeConnect(hardwareManager()->networkManager(), "TODO", "TODO", this); - connect(homeConnect, &HomeConnect::connectionChanged, this, &IntegrationPluginHomeConnect::onConnectionChanged); - connect(homeConnect, &HomeConnect::actionExecuted, this, &IntegrationPluginHomeConnect::onActionExecuted); - connect(homeConnect, &HomeConnect::authenticationStatusChanged, this, &IntegrationPluginHomeConnect::onAuthenticationStatusChanged); - homeConnect->getAccessTokenFromRefreshToken(refreshToken); - m_homeConnectConnections.insert(thing, homeConnect); - info->finish(Thing::ThingErrorNoError); - } - } else { - info->finish(Thing::ThingErrorThingClassNotFound); - } -} - -void IntegrationPluginHomeConnect::postSetupThing(Thing *thing) -{ if (thing->thingClassId() == homeConnectConnectionThingClassId) { HomeConnect *homeConnect = m_homeConnectConnections.value(thing); Q_UNUSED(homeConnect) @@ -196,12 +193,26 @@ void IntegrationPluginHomeConnect::postSetupThing(Thing *thing) void IntegrationPluginHomeConnect::executeAction(ThingActionInfo *info) { + Thing *thing = info->thing(); + Action action = info->action(); + if (thing->thingClassId() == homeConnectConnectionThingClassId) { + if (action.actionTypeId() == ActionTypeId("asdf")) { //TODO + } else { + Q_ASSERT_X(false, "executeAction", QString("Unhandled actionTypeId: %1").arg(action.actionTypeId().toString()).toUtf8()); + } + } else { + Q_ASSERT_X(false, "executeAction", QString("Unhandled deviceClassId: %1").arg(thing->thingClassId().toString()).toUtf8()); + } } void IntegrationPluginHomeConnect::thingRemoved(Thing *thing) { qCDebug(dcHomeConnect) << "Delete " << thing->name(); + if (thing->thingClassId() == homeConnectConnectionThingClassId) { + m_homeConnectConnections.take(thing)->deleteLater(); + } + if (myThings().empty()) { hardwareManager()->pluginTimerManager()->unregisterTimer(m_pluginTimer5sec); hardwareManager()->pluginTimerManager()->unregisterTimer(m_pluginTimer60sec); @@ -209,7 +220,6 @@ void IntegrationPluginHomeConnect::thingRemoved(Thing *thing) m_pluginTimer60sec = nullptr; } } -Device *device void IntegrationPluginHomeConnect::onConnectionChanged(bool connected) { @@ -222,37 +232,38 @@ void IntegrationPluginHomeConnect::onConnectionChanged(bool connected) void IntegrationPluginHomeConnect::onAuthenticationStatusChanged(bool authenticated) { - HomeConnect *HomeConnectConnection = static_cast(sender()); - Thing *thing = m_homeConnectConnections.key(HomeConnectConnection); - if (!thing) - return; - - if (!thing->setupComplete()) { + HomeConnect *homeConnectConnection = static_cast(sender()); + if (m_asyncSetup.contains(homeConnectConnection)) { + ThingSetupInfo *info = m_asyncSetup.take(homeConnectConnection); if (authenticated) { - //emit deviceSetupFinished(device, Device::DeviceSetupStatusSuccess); + info->finish(Thing::ThingErrorNoError); } else { - //emit deviceSetupFinished(device, Device::DeviceSetupStatusFailure); + info->finish(Thing::ThingErrorHardwareFailure); } } else { + Thing *thing = m_homeConnectConnections.key(homeConnectConnection); + if (!thing) + return; + thing->setStateValue(homeConnectConnectionLoggedInStateTypeId, authenticated); if (!authenticated) { //refresh access token needs to be refreshed pluginStorage()->beginGroup(thing->id().toString()); QByteArray refreshToken = pluginStorage()->value("refresh_token").toByteArray(); pluginStorage()->endGroup(); - HomeConnectConnection->getAccessTokenFromRefreshToken(refreshToken); + homeConnectConnection->getAccessTokenFromRefreshToken(refreshToken); } } } -void IntegrationPluginHomeConnect::onActionExecuted(QUuid HomeConnectActionId, bool success) +void IntegrationPluginHomeConnect::onRequestExecuted(QUuid requestId, bool success) { - if (m_pendingActions.contains(HomeConnectActionId)) { - ActionId nymeaActionId = m_pendingActions.value(HomeConnectActionId); + if (m_pendingActions.contains(requestId)) { + ThingActionInfo *info = m_pendingActions.value(requestId); if (success) { - //emit actionExecutionFinished(nymeaActionId, Device::DeviceErrorNoError); + info->finish(Thing::ThingErrorNoError); } else { - //emit actionExecutionFinished(nymeaActionId, Device::DeviceErrorHardwareFailure); + info->finish(Thing::ThingErrorHardwareNotAvailable); } } } diff --git a/homeconnect/integrationpluginhomeconnect.h b/homeconnect/integrationpluginhomeconnect.h index 68712607..e7cbbe2a 100644 --- a/homeconnect/integrationpluginhomeconnect.h +++ b/homeconnect/integrationpluginhomeconnect.h @@ -41,7 +41,7 @@ class IntegrationPluginHomeConnect : public IntegrationPlugin { Q_OBJECT - Q_PLUGIN_METADATA(IID "io.nymea.IntegrationPlugin" FILE "IntegrationPluginHomeConnect.json") + Q_PLUGIN_METADATA(IID "io.nymea.IntegrationPlugin" FILE "integrationpluginhomeconnect.json") Q_INTERFACES(IntegrationPlugin) public: @@ -62,15 +62,17 @@ private: PluginTimer *m_pluginTimer5sec = nullptr; PluginTimer *m_pluginTimer60sec = nullptr; + QHash m_asyncSetup; + QHash m_setupHomeConnectConnections; QHash m_homeConnectConnections; - QHash m_pendingActions; + QHash m_pendingActions; private slots: void onConnectionChanged(bool connected); void onAuthenticationStatusChanged(bool authenticated); - void onActionExecuted(QUuid actionId, bool success); + void onRequestExecuted(QUuid requestId, bool success); }; #endif // INTEGRATIONPLUGINHOMECONNECT_H diff --git a/homeconnect/integrationpluginhomeconnect.json b/homeconnect/integrationpluginhomeconnect.json index 41186b7b..ba26e672 100644 --- a/homeconnect/integrationpluginhomeconnect.json +++ b/homeconnect/integrationpluginhomeconnect.json @@ -44,6 +44,126 @@ "type": "QString" } ] + }, + { + "id": "96845b7d-4c20-43a0-a810-ec505df3ee88", + "name": "oven", + "displayName": "Oven", + "interfaces": ["connectable"], + "createMethods": ["auto"], + "paramTypes": [ + ], + "stateTypes": [ + { + "id": "e0a6c618-d849-4206-9e3c-cd01352664e7", + "name": "connected", + "displayName": "Connected", + "displayNameEvent": "Connected changed", + "defaultValue": true, + "cached": false, + "type": "bool" + } + ] + }, + { + "id": "bb0fcd0b-9594-4368-99d9-3ad5e5a8136b", + "name": "dishwasher", + "displayName": "Dishwasher", + "interfaces": ["connectable"], + "createMethods": ["auto"], + "paramTypes": [ + ], + "stateTypes": [ + { + "id": "7c056989-d91b-492c-9206-ef77fb81b0c8", + "name": "connected", + "displayName": "Connected", + "displayNameEvent": "Connected changed", + "defaultValue": true, + "cached": false, + "type": "bool" + } + ] + }, + { + "id": "f6b39ce2-8276-4db7-b2a3-4a04cafacbb9", + "name": "coffeMachine", + "displayName": "Coffe Machine", + "interfaces": ["connectable"], + "createMethods": ["auto"], + "paramTypes": [ + ], + "stateTypes": [ + { + "id": "796aa7d3-db32-4b6a-88b0-323813feceb3", + "name": "connected", + "displayName": "Connected", + "displayNameEvent": "Connected changed", + "defaultValue": true, + "cached": false, + "type": "bool" + } + ] + }, + { + "id": "f360cc43-41fc-454a-b6df-09ec0a66c22a", + "name": "dryer", + "displayName": "Dryer", + "interfaces": ["connectable"], + "createMethods": ["auto"], + "paramTypes": [ + ], + "stateTypes": [ + { + "id": "485f895a-5c2d-4e1a-8f77-a2d020363635", + "name": "connected", + "displayName": "Connected", + "displayNameEvent": "Connected changed", + "defaultValue": true, + "cached": false, + "type": "bool" + } + ] + }, + { + "id": "6cbf309d-bde8-4e6e-ad6d-b85c8fc1843f", + "name": "fridge", + "displayName": "Fridge", + "interfaces": ["connectable"], + "createMethods": ["auto"], + "paramTypes": [ + ], + "stateTypes": [ + { + "id": "16931afd-44f6-4b13-bd3e-f6d30ac54ea0", + "name": "connected", + "displayName": "Connected", + "displayNameEvent": "Connected changed", + "defaultValue": true, + "cached": false, + "type": "bool" + } + ] + }, + { + "id": "aaec390e-a61f-40ea-b42c-80f69428690b", + "name": "washer", + "displayName": "Washer", + "interfaces": ["connectable"], + "createMethods": ["auto"], + "paramTypes": [ + ], + "stateTypes": [ + { + "id": "950a8bf0-83c4-4e1b-9c00-167a4d3e3c22", + "name": "connected", + "displayName": "Connected", + "displayNameEvent": "Connected changed", + "defaultValue": true, + "cached": false, + "type": "bool" + } + ] } ] }