Merge PR #259: New Plugin: Home Connect
This commit is contained in:
commit
ca3b21333b
16
debian/control
vendored
16
debian/control
vendored
@ -387,6 +387,21 @@ Description: nymea.io plugin for gpio
|
|||||||
This package will install the nymea.io plugin for gpio
|
This package will install the nymea.io plugin for gpio
|
||||||
|
|
||||||
|
|
||||||
|
Package: nymea-plugin-homeconnect
|
||||||
|
Architecture: any
|
||||||
|
Depends: ${shlibs:Depends},
|
||||||
|
${misc:Depends},
|
||||||
|
nymea-plugins-translations,
|
||||||
|
Description: nymea.io plugin for home connect
|
||||||
|
The nymea daemon is a plugin based IoT (Internet of Things) server. The
|
||||||
|
server works like a translator for devices, things and services and
|
||||||
|
allows them to interact.
|
||||||
|
With the powerful rule engine you are able to connect any device available
|
||||||
|
in the system and create individual scenes and behaviors for your environment.
|
||||||
|
.
|
||||||
|
This package will install the nymea.io plugin for home connect appliances
|
||||||
|
|
||||||
|
|
||||||
Package: nymea-plugin-i2cdevices
|
Package: nymea-plugin-i2cdevices
|
||||||
Architecture: any
|
Architecture: any
|
||||||
Section: libs
|
Section: libs
|
||||||
@ -1058,6 +1073,7 @@ Depends: nymea-plugin-anel,
|
|||||||
nymea-plugin-flowercare,
|
nymea-plugin-flowercare,
|
||||||
nymea-plugin-fronius,
|
nymea-plugin-fronius,
|
||||||
nymea-plugin-genericthings,
|
nymea-plugin-genericthings,
|
||||||
|
nymea-plugin-homeconnect,
|
||||||
nymea-plugin-kodi,
|
nymea-plugin-kodi,
|
||||||
nymea-plugin-lgsmarttv,
|
nymea-plugin-lgsmarttv,
|
||||||
nymea-plugin-lifx,
|
nymea-plugin-lifx,
|
||||||
|
|||||||
1
debian/nymea-plugin-homeconnect.install.in
vendored
Normal file
1
debian/nymea-plugin-homeconnect.install.in
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
usr/lib/@DEB_HOST_MULTIARCH@/nymea/plugins/libnymea_integrationpluginhomeconnect.so
|
||||||
38
homeconnect/README.md
Normal file
38
homeconnect/README.md
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
# Home Connect
|
||||||
|
|
||||||
|
Connects your Home Connect home appliances to nymea.
|
||||||
|
|
||||||
|
## Supported Things
|
||||||
|
|
||||||
|
Home connected home appliances, like:
|
||||||
|
* Oven
|
||||||
|
* Dishwasher
|
||||||
|
* Coffee maker
|
||||||
|
* Dryer
|
||||||
|
* Fridge
|
||||||
|
* Washer
|
||||||
|
* Cook Top
|
||||||
|
* Hood
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
* The package “nymea-plugin-homeconnect” must be installed
|
||||||
|
* Internet connection
|
||||||
|
* Home connect account
|
||||||
|
|
||||||
|
## Plug-In Settings
|
||||||
|
|
||||||
|
**Simulation Mode**
|
||||||
|
HomeConnect has a simulation server, where the home appliances are simulated. This feature is useful for demonstration and development.
|
||||||
|
|
||||||
|
**Control**
|
||||||
|
The control of the oven for example is only allowed with an authorized API key. The nymea community
|
||||||
|
API credentials does not have this permission. Enabling this without the permission will result in failing login attempts.
|
||||||
|
|
||||||
|
**Custom client key and secret**
|
||||||
|
You can register as developer on https://developer.home-connect.com, where you can obtain you own client credentials. The default client credentials are made available through the nymea community API key provider.
|
||||||
|
|
||||||
|
## More
|
||||||
|
|
||||||
|
Home Connect developer documentation:
|
||||||
|
https://developer.home-connect.com/
|
||||||
903
homeconnect/homeconnect.cpp
Normal file
903
homeconnect/homeconnect.cpp
Normal file
@ -0,0 +1,903 @@
|
|||||||
|
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
|
||||||
|
*
|
||||||
|
* 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 <https://www.gnu.org/licenses/>.
|
||||||
|
*
|
||||||
|
* 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 "homeconnect.h"
|
||||||
|
#include "extern-plugininfo.h"
|
||||||
|
|
||||||
|
#include <QJsonDocument>
|
||||||
|
#include <QJsonObject>
|
||||||
|
#include <QJsonArray>
|
||||||
|
#include <QUrlQuery>
|
||||||
|
|
||||||
|
HomeConnect::HomeConnect(NetworkAccessManager *networkmanager, const QByteArray &clientKey, const QByteArray &clientSecret, bool simulationMode, QObject *parent) :
|
||||||
|
QObject(parent),
|
||||||
|
m_clientKey(clientKey),
|
||||||
|
m_clientSecret(clientSecret),
|
||||||
|
m_networkManager(networkmanager)
|
||||||
|
|
||||||
|
{
|
||||||
|
m_tokenRefreshTimer = new QTimer(this);
|
||||||
|
m_tokenRefreshTimer->setSingleShot(true);
|
||||||
|
connect(m_tokenRefreshTimer, &QTimer::timeout, this, &HomeConnect::onRefreshTimeout);
|
||||||
|
setSimulationMode(simulationMode);
|
||||||
|
}
|
||||||
|
|
||||||
|
QByteArray HomeConnect::accessToken()
|
||||||
|
{
|
||||||
|
return m_accessToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
QByteArray HomeConnect::refreshToken()
|
||||||
|
{
|
||||||
|
return m_refreshToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
void HomeConnect::setSimulationMode(bool simulation)
|
||||||
|
{
|
||||||
|
m_simulationMode = simulation;
|
||||||
|
if (simulation) {
|
||||||
|
m_baseAuthorizationUrl = "https://simulator.home-connect.com/security/oauth/authorize";
|
||||||
|
m_baseTokenUrl = "https://simulator.home-connect.com/security/oauth/token";
|
||||||
|
m_baseControlUrl = "https://simulator.home-connect.com";
|
||||||
|
} else {
|
||||||
|
m_baseAuthorizationUrl = "https://api.home-connect.com/security/oauth/authorize";
|
||||||
|
m_baseTokenUrl = "https://api.home-connect.com/security/oauth/token";
|
||||||
|
m_baseControlUrl = "https://api.home-connect.com";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
QUrl HomeConnect::getLoginUrl(const QUrl &redirectUrl, const QString &scope)
|
||||||
|
{
|
||||||
|
if (m_clientKey.isEmpty()) {
|
||||||
|
qWarning(dcHomeConnect) << "Client key not defined!";
|
||||||
|
return QUrl("");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (redirectUrl.isEmpty()){
|
||||||
|
qWarning(dcHomeConnect) << "No redirect uri defined!";
|
||||||
|
}
|
||||||
|
m_redirectUri = QUrl::toPercentEncoding(redirectUrl.toString());
|
||||||
|
|
||||||
|
QUrl url(m_baseAuthorizationUrl);
|
||||||
|
QUrlQuery queryParams;
|
||||||
|
queryParams.addQueryItem("client_id", m_clientKey);
|
||||||
|
queryParams.addQueryItem("redirect_uri", m_redirectUri);
|
||||||
|
queryParams.addQueryItem("response_type", "code");
|
||||||
|
queryParams.addQueryItem("scope", scope);
|
||||||
|
queryParams.addQueryItem("state", QUuid::createUuid().toString());
|
||||||
|
queryParams.addQueryItem("nonce", QUuid::createUuid().toString());
|
||||||
|
m_codeChallenge = QUuid::createUuid().toString().remove(QRegExp("[{}-]"));
|
||||||
|
queryParams.addQueryItem("code_challenge", m_codeChallenge);
|
||||||
|
queryParams.addQueryItem("code_challenge_method", "plain");
|
||||||
|
url.setQuery(queryParams);
|
||||||
|
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
|
||||||
|
void HomeConnect::onRefreshTimeout()
|
||||||
|
{
|
||||||
|
qCDebug(dcHomeConnect) << "Refresh authentication token";
|
||||||
|
getAccessTokenFromRefreshToken(m_refreshToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool HomeConnect::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(dcHomeConnect()) << "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(dcHomeConnect()) << "Client token provided doesn’t correspond to client that generated auth code.";
|
||||||
|
}
|
||||||
|
if(jsonDoc.toVariant().toMap().value("error").toString() == "invalid_redirect_uri") {
|
||||||
|
qWarning(dcHomeConnect()) << "Missing redirect_uri parameter.";
|
||||||
|
}
|
||||||
|
if(jsonDoc.toVariant().toMap().value("error").toString() == "invalid_code") {
|
||||||
|
qWarning(dcHomeConnect()) << "Expired authorization code.";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setAuthenticated(false);
|
||||||
|
return false;
|
||||||
|
case 401:
|
||||||
|
qWarning(dcHomeConnect()) << "Client does not have permission to use this API.";
|
||||||
|
setAuthenticated(false);
|
||||||
|
return false;
|
||||||
|
case 403:
|
||||||
|
qCWarning(dcHomeConnect()) << "Forbidden, Scope has not been granted or home appliance is not assigned to HC account";
|
||||||
|
setAuthenticated(false);
|
||||||
|
return false;
|
||||||
|
case 404:
|
||||||
|
qCWarning(dcHomeConnect()) << "Not Found. This resource is not available (e.g. no images on washing machine)";
|
||||||
|
return false;
|
||||||
|
case 405:
|
||||||
|
qWarning(dcHomeConnect()) << "Wrong HTTP method used.";
|
||||||
|
setAuthenticated(false);
|
||||||
|
return false;
|
||||||
|
case 408:
|
||||||
|
qCWarning(dcHomeConnect())<< "Request Timeout, API Server failed to produce an answer or has no connection to backend service";
|
||||||
|
return false;
|
||||||
|
case 409:
|
||||||
|
qCWarning(dcHomeConnect()) << "Conflict - Command/Query cannot be executed for the home appliance, the error response contains the error details";
|
||||||
|
qCWarning(dcHomeConnect()) << "Error" << jsonDoc;
|
||||||
|
return false;
|
||||||
|
case 415:
|
||||||
|
qCWarning(dcHomeConnect())<< "Unsupported Media Type. The request's Content-Type is not supported";
|
||||||
|
return false;
|
||||||
|
case 429:
|
||||||
|
qCWarning(dcHomeConnect())<< "Too Many Requests, the number of requests for a specific endpoint exceeded the quota of the client";
|
||||||
|
return false;
|
||||||
|
case 500:
|
||||||
|
qCWarning(dcHomeConnect())<< "Internal Server Error, in case of a server configuration error or any errors in resource files";
|
||||||
|
return false;
|
||||||
|
case 503:
|
||||||
|
qCWarning(dcHomeConnect())<< "Service Unavailable,if a required backend service is not available";
|
||||||
|
return false;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error.error != QJsonParseError::NoError) {
|
||||||
|
qCWarning(dcHomeConnect()) << "Received invalide JSON object" << rawData;
|
||||||
|
qCWarning(dcHomeConnect()) << "Status" << status;
|
||||||
|
setAuthenticated(false);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
setAuthenticated(true);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void HomeConnect::getAccessTokenFromRefreshToken(const QByteArray &refreshToken)
|
||||||
|
{
|
||||||
|
if (refreshToken.isEmpty()) {
|
||||||
|
qWarning(dcHomeConnect) << "No refresh token given!";
|
||||||
|
setAuthenticated(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
QUrl url(m_baseTokenUrl);
|
||||||
|
QUrlQuery query;
|
||||||
|
query.clear();
|
||||||
|
query.addQueryItem("grant_type", "refresh_token");
|
||||||
|
query.addQueryItem("refresh_token", refreshToken);
|
||||||
|
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](){
|
||||||
|
|
||||||
|
QByteArray rawData = reply->readAll();
|
||||||
|
if (!checkStatusCode(reply, rawData)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
QJsonDocument data = QJsonDocument::fromJson(rawData);
|
||||||
|
|
||||||
|
if(!data.toVariant().toMap().contains("access_token")) {
|
||||||
|
setAuthenticated(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
m_accessToken = data.toVariant().toMap().value("access_token").toByteArray();
|
||||||
|
emit receivedAccessToken(m_accessToken);
|
||||||
|
|
||||||
|
if (data.toVariant().toMap().contains("expires_in")) {
|
||||||
|
int expireTime = data.toVariant().toMap().value("expires_in").toInt();
|
||||||
|
qCDebug(dcHomeConnect) << "Access token expires int" << expireTime << "s, at" << QDateTime::currentDateTime().addSecs(expireTime).toString();
|
||||||
|
if (!m_tokenRefreshTimer) {
|
||||||
|
qWarning(dcHomeConnect()) << "Access token refresh timer not initialized";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (expireTime < 20) {
|
||||||
|
qCWarning(dcHomeConnect()) << "Expire time too short";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
m_tokenRefreshTimer->start((expireTime - 20) * 1000);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void HomeConnect::getAccessTokenFromAuthorizationCode(const QByteArray &authorizationCode)
|
||||||
|
{
|
||||||
|
// Obtaining access token
|
||||||
|
if(authorizationCode.isEmpty())
|
||||||
|
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_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("code_verifier", m_codeChallenge);
|
||||||
|
|
||||||
|
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").toInt();
|
||||||
|
qCDebug(dcHomeConnect()) << "Token expires in" << expireTime << "s, at" << QDateTime::currentDateTime().addSecs(expireTime).toString();
|
||||||
|
if (!m_tokenRefreshTimer) {
|
||||||
|
qWarning(dcHomeConnect()) << "Token refresh timer not initialized";
|
||||||
|
setAuthenticated(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (expireTime < 20) {
|
||||||
|
qCWarning(dcHomeConnect()) << "Expire time too short";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
m_tokenRefreshTimer->start((expireTime - 20) * 1000);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void HomeConnect::getHomeAppliances()
|
||||||
|
{
|
||||||
|
QUrl url = QUrl(m_baseControlUrl+"/api/homeappliances");
|
||||||
|
|
||||||
|
QNetworkRequest request(url);
|
||||||
|
request.setRawHeader("Authorization", "Bearer "+m_accessToken);
|
||||||
|
request.setRawHeader("accept", "application/vnd.bsh.sdk.v1+json");
|
||||||
|
|
||||||
|
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 dataMap = QJsonDocument::fromJson(rawData).toVariant().toMap().value("data").toMap();
|
||||||
|
|
||||||
|
QList<HomeAppliance> appliances;
|
||||||
|
foreach (const QVariant &variant, dataMap.value("homeappliances").toList()) {
|
||||||
|
QVariantMap obj = variant.toMap();
|
||||||
|
HomeAppliance appliance;
|
||||||
|
appliance.name = obj["name"].toString();
|
||||||
|
appliance.brand = obj["brand"].toString();
|
||||||
|
appliance.vib = obj["vib"].toString();
|
||||||
|
appliance.type = obj["type"].toString();
|
||||||
|
appliance.homeApplianceId = obj["haId"].toString();
|
||||||
|
appliance.enumber = obj["enumber"].toString();
|
||||||
|
appliance.connected = obj["connected"].toBool();
|
||||||
|
appliances.append(appliance);
|
||||||
|
}
|
||||||
|
if (!appliances.isEmpty())
|
||||||
|
emit receivedHomeAppliances(appliances);
|
||||||
|
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void HomeConnect::getPrograms(const QString &haId)
|
||||||
|
{
|
||||||
|
QUrl url = QUrl(m_baseControlUrl+"/api/homeappliances/"+haId+"/programs");
|
||||||
|
|
||||||
|
QNetworkRequest request(url);
|
||||||
|
request.setRawHeader("Authorization", "Bearer "+m_accessToken);
|
||||||
|
request.setRawHeader("Accept-Language", "en-US");
|
||||||
|
request.setRawHeader("accept", "application/vnd.bsh.sdk.v1+json");
|
||||||
|
|
||||||
|
QNetworkReply *reply = m_networkManager->get(request);
|
||||||
|
connect(reply, &QNetworkReply::finished, reply, &QNetworkReply::deleteLater);
|
||||||
|
connect(reply, &QNetworkReply::finished, this, [this, haId, reply]{
|
||||||
|
|
||||||
|
QByteArray rawData = reply->readAll();
|
||||||
|
if (!checkStatusCode(reply, rawData)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
QVariantMap dataMap = QJsonDocument::fromJson(rawData).toVariant().toMap().value("data").toMap();
|
||||||
|
QVariantList programList = dataMap.value("programs").toList();
|
||||||
|
QStringList programs;
|
||||||
|
Q_FOREACH(QVariant var, programList) {
|
||||||
|
if (var.toMap().contains("key")) {
|
||||||
|
programs.append(var.toMap().value("key").toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!programs.isEmpty())
|
||||||
|
emit receivedPrograms(haId, programs);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void HomeConnect::getProgramsAvailable(const QString &haId)
|
||||||
|
{
|
||||||
|
QUrl url = QUrl(m_baseControlUrl+"/api/homeappliances/"+haId+"/programs/available");
|
||||||
|
|
||||||
|
QNetworkRequest request(url);
|
||||||
|
request.setRawHeader("Authorization", "Bearer "+m_accessToken);
|
||||||
|
request.setRawHeader("Accept-Language", "en-US");
|
||||||
|
request.setRawHeader("accept", "application/vnd.bsh.sdk.v1+json");
|
||||||
|
|
||||||
|
QNetworkReply *reply = m_networkManager->get(request);
|
||||||
|
connect(reply, &QNetworkReply::finished, reply, &QNetworkReply::deleteLater);
|
||||||
|
connect(reply, &QNetworkReply::finished, this, [this, haId, reply]{
|
||||||
|
|
||||||
|
QByteArray rawData = reply->readAll();
|
||||||
|
if (!checkStatusCode(reply, rawData)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
QVariantMap dataMap = QJsonDocument::fromJson(rawData).toVariant().toMap().value("data").toMap();
|
||||||
|
QVariantList programList = dataMap.value("programs").toList();
|
||||||
|
QStringList programs;
|
||||||
|
Q_FOREACH(QVariant var, programList) {
|
||||||
|
if (var.toMap().contains("key")) {
|
||||||
|
programs.append(var.toMap().value("key").toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!programs.isEmpty())
|
||||||
|
emit receivedAvailablePrograms(haId, programs);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void HomeConnect::getProgramsActive(const QString &haId)
|
||||||
|
{
|
||||||
|
QUrl url = QUrl(m_baseControlUrl+"/api/homeappliances/"+haId+"/programs/active");
|
||||||
|
|
||||||
|
QNetworkRequest request(url);
|
||||||
|
request.setRawHeader("Authorization", "Bearer "+m_accessToken);
|
||||||
|
request.setRawHeader("Accept-Language", "en-US");
|
||||||
|
request.setRawHeader("accept", "application/vnd.bsh.sdk.v1+json");
|
||||||
|
|
||||||
|
QNetworkReply *reply = m_networkManager->get(request);
|
||||||
|
connect(reply, &QNetworkReply::finished, reply, &QNetworkReply::deleteLater);
|
||||||
|
connect(reply, &QNetworkReply::finished, this, [this, haId, reply]{
|
||||||
|
|
||||||
|
QByteArray rawData = reply->readAll();
|
||||||
|
if (!checkStatusCode(reply, rawData)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
QVariantMap map = QJsonDocument::fromJson(rawData).toVariant().toMap();
|
||||||
|
QHash<QString, QVariant> options;
|
||||||
|
if (map.contains("data")) {
|
||||||
|
QString key = map.value("data").toMap().value("key").toString();
|
||||||
|
Q_FOREACH(QVariant var, map.value("data").toMap().value("options").toList()) {
|
||||||
|
options.insert(var.toMap().value("key").toString(), var.toMap().value("value"));
|
||||||
|
}
|
||||||
|
emit receivedActiveProgram(haId, key, options);
|
||||||
|
} else if (map.contains("error")) {
|
||||||
|
QString key = map.value("error").toMap().value("key").toString();
|
||||||
|
emit receivedActiveProgram(haId, key, options);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void HomeConnect::getProgramsSelected(const QString &haId)
|
||||||
|
{
|
||||||
|
QUrl url = QUrl(m_baseControlUrl+"/api/homeappliances/"+haId+"/programs/selected");
|
||||||
|
|
||||||
|
QNetworkRequest request(url);
|
||||||
|
request.setRawHeader("Authorization", "Bearer "+m_accessToken);
|
||||||
|
request.setRawHeader("Accept-Language", "en-US");
|
||||||
|
request.setRawHeader("accept", "application/vnd.bsh.sdk.v1+json");
|
||||||
|
|
||||||
|
QNetworkReply *reply = m_networkManager->get(request);
|
||||||
|
connect(reply, &QNetworkReply::finished, reply, &QNetworkReply::deleteLater);
|
||||||
|
connect(reply, &QNetworkReply::finished, this, [this, haId, reply]{
|
||||||
|
|
||||||
|
QByteArray rawData = reply->readAll();
|
||||||
|
if (!checkStatusCode(reply, rawData)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
QVariantMap map = QJsonDocument::fromJson(rawData).toVariant().toMap();
|
||||||
|
QHash<QString, QVariant> options;
|
||||||
|
if (map.contains("data")) {
|
||||||
|
QString key = map.value("data").toMap().value("key").toString();
|
||||||
|
Q_FOREACH(QVariant var, map.value("data").toMap().value("options").toList()) {
|
||||||
|
options.insert(var.toMap().value("key").toString(), var.toMap().value("value"));
|
||||||
|
}
|
||||||
|
emit receivedSelectedProgram(haId, key, options);
|
||||||
|
} else if (map.contains("error")) {
|
||||||
|
QString key = map.value("error").toMap().value("key").toString();
|
||||||
|
emit receivedSelectedProgram(haId, key, options);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void HomeConnect::getProgramsActiveOption(const QString &haId, const QString &optionKey)
|
||||||
|
{
|
||||||
|
QUrl url = QUrl(m_baseControlUrl+"/api/homeappliances/"+haId+"/programs/active/options/"+optionKey);
|
||||||
|
|
||||||
|
QNetworkRequest request(url);
|
||||||
|
request.setRawHeader("Authorization", "Bearer "+m_accessToken);
|
||||||
|
request.setRawHeader("Accept-Language", "en-US");
|
||||||
|
request.setRawHeader("accept", "application/vnd.bsh.sdk.v1+json");
|
||||||
|
|
||||||
|
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 dataMap = QJsonDocument::fromJson(rawData).toVariant().toMap().value("data").toMap();
|
||||||
|
qCDebug(dcHomeConnect()) << "key" << dataMap.value("key").toString() << "value" << dataMap.value("value").toString() << dataMap.value("unit").toString();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
QUuid HomeConnect::selectProgram(const QString &haId, const QString &programKey, QList<HomeConnect::Option> options)
|
||||||
|
{
|
||||||
|
QUuid commandId = QUuid::createUuid();
|
||||||
|
QUrl url = QUrl(m_baseControlUrl+"/api/homeappliances/"+haId+"/programs/selected");
|
||||||
|
|
||||||
|
QNetworkRequest request(url);
|
||||||
|
request.setRawHeader("Authorization", "Bearer "+m_accessToken);
|
||||||
|
request.setRawHeader("Accept-Language", "en-US");
|
||||||
|
request.setRawHeader("accept", "application/vnd.bsh.sdk.v1+json");
|
||||||
|
request.setHeader(QNetworkRequest::KnownHeaders::ContentTypeHeader, "application/vnd.bsh.sdk.v1+json");
|
||||||
|
|
||||||
|
QJsonDocument doc;
|
||||||
|
QVariantMap data;
|
||||||
|
data.insert("key", programKey);
|
||||||
|
if (!options.isEmpty()) {
|
||||||
|
QVariantList optionsArray;
|
||||||
|
Q_FOREACH(Option option, options) {
|
||||||
|
QVariantMap optionObject;
|
||||||
|
optionObject["key"] = option.key;
|
||||||
|
optionObject["value"] = option.value;
|
||||||
|
if (!option.unit.isEmpty())
|
||||||
|
optionObject["unit"] = option.unit;
|
||||||
|
optionsArray.append(optionObject);
|
||||||
|
}
|
||||||
|
data.insert("options", optionsArray);
|
||||||
|
}
|
||||||
|
QVariantMap obj;
|
||||||
|
obj.insert("data", data);
|
||||||
|
doc.setObject(QJsonObject::fromVariantMap(obj));
|
||||||
|
QNetworkReply *reply = m_networkManager->put(request, doc.toJson());
|
||||||
|
connect(reply, &QNetworkReply::finished, reply, &QNetworkReply::deleteLater);
|
||||||
|
connect(reply, &QNetworkReply::finished, this, [this, commandId, reply]{
|
||||||
|
|
||||||
|
int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
|
||||||
|
if (status != 204) {
|
||||||
|
emit commandExecuted(commandId, false);
|
||||||
|
QByteArray rawData = reply->readAll();
|
||||||
|
qCDebug(dcHomeConnect()) << "Selected program" << rawData;
|
||||||
|
QJsonParseError error;
|
||||||
|
QJsonDocument data = QJsonDocument::fromJson(rawData, &error);
|
||||||
|
if (error.error != QJsonParseError::NoError) {
|
||||||
|
qCDebug(dcHomeConnect()) << "Selected program: Received invalide JSON object";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (data.toVariant().toMap().contains("error")) {
|
||||||
|
qCWarning(dcHomeConnect()) << "Start program" << data.toVariant().toMap().value("error").toMap().value("description").toString();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
emit commandExecuted(commandId, true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return commandId;
|
||||||
|
}
|
||||||
|
|
||||||
|
QUuid HomeConnect::setSelectedProgramOptions(const QString &haId, QList<HomeConnect::Option> options)
|
||||||
|
{
|
||||||
|
if (options.isEmpty())
|
||||||
|
return "";
|
||||||
|
|
||||||
|
QUuid commandId = QUuid::createUuid();
|
||||||
|
QUrl url = QUrl(m_baseControlUrl+"/api/homeappliances/"+haId+"/programs/selected/options");
|
||||||
|
|
||||||
|
QNetworkRequest request(url);
|
||||||
|
request.setRawHeader("Authorization", "Bearer "+m_accessToken);
|
||||||
|
request.setRawHeader("Accept-Language", "en-US");
|
||||||
|
request.setRawHeader("accept", "application/vnd.bsh.sdk.v1+json");
|
||||||
|
request.setHeader(QNetworkRequest::KnownHeaders::ContentTypeHeader, "application/vnd.bsh.sdk.v1+json");
|
||||||
|
|
||||||
|
QJsonDocument doc;
|
||||||
|
QVariantMap data;
|
||||||
|
QVariantList optionsArray;
|
||||||
|
Q_FOREACH(Option option, options) {
|
||||||
|
QVariantMap optionObject;
|
||||||
|
optionObject["key"] = option.key;
|
||||||
|
optionObject["value"] = option.value;
|
||||||
|
if (!option.unit.isEmpty())
|
||||||
|
optionObject["unit"] = option.unit;
|
||||||
|
optionsArray.append(optionObject);
|
||||||
|
}
|
||||||
|
data.insert("options", optionsArray);
|
||||||
|
QVariantMap obj;
|
||||||
|
obj.insert("data", data);
|
||||||
|
doc.setObject(QJsonObject::fromVariantMap(obj));
|
||||||
|
qCDebug(dcHomeConnect()) << "Selected Program Options" << doc.toJson();
|
||||||
|
|
||||||
|
QNetworkReply *reply = m_networkManager->put(request, doc.toJson());
|
||||||
|
connect(reply, &QNetworkReply::finished, reply, &QNetworkReply::deleteLater);
|
||||||
|
connect(reply, &QNetworkReply::finished, this, [this, commandId, reply]{
|
||||||
|
|
||||||
|
int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
|
||||||
|
if (status != 204) {
|
||||||
|
emit commandExecuted(commandId, false);
|
||||||
|
QByteArray rawData = reply->readAll();
|
||||||
|
qCDebug(dcHomeConnect()) << "Selected program" << rawData;
|
||||||
|
QJsonParseError error;
|
||||||
|
QJsonDocument data = QJsonDocument::fromJson(rawData, &error);
|
||||||
|
if (error.error != QJsonParseError::NoError) {
|
||||||
|
qCDebug(dcHomeConnect()) << "Selected program options: Received invalide JSON object";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (data.toVariant().toMap().contains("error")) {
|
||||||
|
qCWarning(dcHomeConnect()) << "Selected program options:" << data.toVariant().toMap().value("error").toMap().value("description").toString();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
emit commandExecuted(commandId, true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return commandId;
|
||||||
|
}
|
||||||
|
|
||||||
|
QUuid HomeConnect::startProgram(const QString &haId, const QString &programKey, QList<HomeConnect::Option> options)
|
||||||
|
{
|
||||||
|
QUuid commandId = QUuid::createUuid();
|
||||||
|
QUrl url = QUrl(m_baseControlUrl+"/api/homeappliances/"+haId+"/programs/active");
|
||||||
|
|
||||||
|
QNetworkRequest request(url);
|
||||||
|
request.setRawHeader("Authorization", "Bearer "+m_accessToken);
|
||||||
|
request.setRawHeader("Accept-Language", "en-US");
|
||||||
|
request.setRawHeader("accept", "application/vnd.bsh.sdk.v1+json");
|
||||||
|
request.setHeader(QNetworkRequest::KnownHeaders::ContentTypeHeader, "application/vnd.bsh.sdk.v1+json");
|
||||||
|
|
||||||
|
QJsonDocument doc;
|
||||||
|
QVariantMap data;
|
||||||
|
data.insert("key", programKey);
|
||||||
|
QVariantList optionsArray;
|
||||||
|
Q_FOREACH(Option option, options) {
|
||||||
|
QVariantMap optionObject;
|
||||||
|
optionObject["key"] = option.key;
|
||||||
|
optionObject["value"] = option.value;
|
||||||
|
if (!option.unit.isEmpty())
|
||||||
|
optionObject["unit"] = option.unit;
|
||||||
|
optionsArray.append(optionObject);
|
||||||
|
}
|
||||||
|
data.insert("options", optionsArray);
|
||||||
|
QVariantMap obj;
|
||||||
|
obj.insert("data", data);
|
||||||
|
doc.setObject(QJsonObject::fromVariantMap(obj));
|
||||||
|
QNetworkReply *reply = m_networkManager->put(request, doc.toJson());
|
||||||
|
connect(reply, &QNetworkReply::finished, reply, &QNetworkReply::deleteLater);
|
||||||
|
connect(reply, &QNetworkReply::finished, this, [this, commandId, reply]{
|
||||||
|
|
||||||
|
int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
|
||||||
|
if (status != 204) {
|
||||||
|
emit commandExecuted(commandId, false);
|
||||||
|
QJsonParseError error;
|
||||||
|
QJsonDocument data = QJsonDocument::fromJson(reply->readAll(), &error);
|
||||||
|
if (error.error != QJsonParseError::NoError) {
|
||||||
|
qCDebug(dcHomeConnect()) << "Start program: Received invalide JSON object";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
qCDebug(dcHomeConnect()) << "Start program response" << data.toJson();
|
||||||
|
if (data.toVariant().toMap().contains("error")) {
|
||||||
|
qCWarning(dcHomeConnect()) << "Start program" << data.toVariant().toMap().value("error").toMap().value("description").toString();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
emit commandExecuted(commandId, true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return commandId;
|
||||||
|
}
|
||||||
|
|
||||||
|
QUuid HomeConnect::stopProgram(const QString &haId)
|
||||||
|
{
|
||||||
|
QUuid commandId = QUuid::createUuid();
|
||||||
|
QUrl url = QUrl(m_baseControlUrl+"/api/homeappliances/"+haId+"/programs/active");
|
||||||
|
|
||||||
|
QNetworkRequest request(url);
|
||||||
|
request.setRawHeader("Authorization", "Bearer "+m_accessToken);
|
||||||
|
request.setRawHeader("Accept-Language", "en-US");
|
||||||
|
request.setRawHeader("accept", "application/vnd.bsh.sdk.v1+json");
|
||||||
|
|
||||||
|
QNetworkReply *reply = m_networkManager->deleteResource(request);
|
||||||
|
connect(reply, &QNetworkReply::finished, reply, &QNetworkReply::deleteLater);
|
||||||
|
connect(reply, &QNetworkReply::finished, this, [this, commandId, reply]{
|
||||||
|
|
||||||
|
|
||||||
|
int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
|
||||||
|
if (status != 204) {
|
||||||
|
emit commandExecuted(commandId, false);
|
||||||
|
} else {
|
||||||
|
emit commandExecuted(commandId, true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return commandId;
|
||||||
|
}
|
||||||
|
|
||||||
|
void HomeConnect::getStatus(const QString &haid)
|
||||||
|
{
|
||||||
|
QUrl url = QUrl(m_baseControlUrl+"/api/homeappliances/"+haid+"/status");
|
||||||
|
|
||||||
|
QNetworkRequest request(url);
|
||||||
|
request.setRawHeader("Authorization", "Bearer "+m_accessToken);
|
||||||
|
request.setRawHeader("Accept-Language", "en-US");
|
||||||
|
request.setRawHeader("accept", "application/vnd.bsh.sdk.v1+json");
|
||||||
|
|
||||||
|
QNetworkReply *reply = m_networkManager->get(request);
|
||||||
|
connect(reply, &QNetworkReply::finished, reply, &QNetworkReply::deleteLater);
|
||||||
|
connect(reply, &QNetworkReply::finished, this, [this, haid, reply]{
|
||||||
|
|
||||||
|
QByteArray rawData = reply->readAll();
|
||||||
|
if (!checkStatusCode(reply, rawData)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
QVariantMap dataMap = QJsonDocument::fromJson(rawData).toVariant().toMap().value("data").toMap();
|
||||||
|
QHash<QString, QVariant> statusList;
|
||||||
|
QVariantList statusVariantList= dataMap.value("status").toList();
|
||||||
|
Q_FOREACH(QVariant status, statusVariantList) {
|
||||||
|
QVariantMap map = status.toMap();
|
||||||
|
statusList.insert(map.value("key").toString(), map.value("value"));
|
||||||
|
}
|
||||||
|
if (!statusList.isEmpty())
|
||||||
|
emit receivedStatusList(haid, statusList);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Get a list of available setting of the home appliance.
|
||||||
|
* Possible Settings:
|
||||||
|
* Power state
|
||||||
|
* Fridge temperature
|
||||||
|
* Fridge super mode
|
||||||
|
* Freezer temperature
|
||||||
|
* Freezer super mode
|
||||||
|
*/
|
||||||
|
|
||||||
|
void HomeConnect::getSettings(const QString &haid)
|
||||||
|
{
|
||||||
|
QUrl url = QUrl(m_baseControlUrl+"/api/homeappliances/"+haid+"/settings");
|
||||||
|
|
||||||
|
QNetworkRequest request(url);
|
||||||
|
request.setRawHeader("Authorization", "Bearer "+m_accessToken);
|
||||||
|
request.setRawHeader("Accept-Language", "en-US");
|
||||||
|
request.setRawHeader("accept", "application/vnd.bsh.sdk.v1+json");
|
||||||
|
|
||||||
|
QNetworkReply *reply = m_networkManager->get(request);
|
||||||
|
connect(reply, &QNetworkReply::finished, reply, &QNetworkReply::deleteLater);
|
||||||
|
connect(reply, &QNetworkReply::finished, this, [this, haid, reply]{
|
||||||
|
|
||||||
|
QByteArray rawData = reply->readAll();
|
||||||
|
if (!checkStatusCode(reply, rawData)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
QVariantMap dataMap = QJsonDocument::fromJson(rawData).toVariant().toMap().value("data").toMap();
|
||||||
|
QVariantList settingsList = dataMap.value("settings").toList();
|
||||||
|
QHash<QString, QVariant> settings;
|
||||||
|
Q_FOREACH(QVariant var, settingsList) {
|
||||||
|
settings.insert(var.toMap().value("key").toString(), var.toMap().value("value"));
|
||||||
|
}
|
||||||
|
if (!settings.isEmpty())
|
||||||
|
emit receivedSettings(haid, settings);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void HomeConnect::connectEventStream()
|
||||||
|
{
|
||||||
|
QUrl url = QUrl(m_baseControlUrl+"/api/homeappliances/events");
|
||||||
|
|
||||||
|
QNetworkRequest request(url);
|
||||||
|
request.setRawHeader("Authorization", "Bearer "+m_accessToken);
|
||||||
|
request.setRawHeader("Accept-Language", "en-US");
|
||||||
|
request.setRawHeader("accept", "text/event-stream");
|
||||||
|
|
||||||
|
QNetworkReply *reply = m_networkManager->get(request);
|
||||||
|
connect(reply, &QNetworkReply::finished, reply, &QNetworkReply::deleteLater);
|
||||||
|
connect(reply, &QNetworkReply::finished, [reply, this] {
|
||||||
|
int reconnectTime = 5000; // Usual reconnect in 5 s
|
||||||
|
if (reply->error() != QNetworkReply::NetworkError::NoError) {
|
||||||
|
qCDebug(dcHomeConnect()) << "Event stream error" << reply->errorString() << reply->readAll();
|
||||||
|
}
|
||||||
|
qCDebug(dcHomeConnect()) << "Eventstream disconected";
|
||||||
|
int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
|
||||||
|
if (status == 429) {
|
||||||
|
reconnectTime = 600000;
|
||||||
|
}
|
||||||
|
qCDebug(dcHomeConnect()) << "Trying to reconnect event stream in" << reconnectTime/1000 << "seconds";
|
||||||
|
QTimer::singleShot(reconnectTime, this, [this] {
|
||||||
|
qCDebug(dcHomeConnect()) << "Reconnecting event stream";
|
||||||
|
connectEventStream();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
connect(reply, &QNetworkReply::readyRead, this, [this, reply]{
|
||||||
|
|
||||||
|
int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
|
||||||
|
if (status == 200) {
|
||||||
|
while (reply->canReadLine()) {
|
||||||
|
QJsonDocument data;
|
||||||
|
QString haId;
|
||||||
|
EventType eventType;
|
||||||
|
|
||||||
|
QByteArray eventTypeLine = reply->readLine();
|
||||||
|
if (eventTypeLine == "\n")
|
||||||
|
continue;
|
||||||
|
if (eventTypeLine.startsWith("event")) {
|
||||||
|
QString eventString = eventTypeLine.split(':').last().trimmed();
|
||||||
|
if (eventString == "KEEP-ALIVE") {
|
||||||
|
eventType = EventTypeKeepAlive;
|
||||||
|
} else if (eventString == "STATUS") {
|
||||||
|
eventType = EventTypeStatus;
|
||||||
|
} else if (eventString == "EVENT") {
|
||||||
|
eventType = EventTypeEvent;
|
||||||
|
} else if (eventString == "NOTIFY") {
|
||||||
|
eventType = EventTypeNotify;
|
||||||
|
} else if (eventString == "DISCONNECTED") {
|
||||||
|
eventType = EventTypeDisconnected;
|
||||||
|
} else if (eventString == "CONNECTED") {
|
||||||
|
eventType = EventTypeConnected;
|
||||||
|
} else if (eventString == "PAIRED") {
|
||||||
|
eventType = EventTypePaired;
|
||||||
|
} else if (eventString == "DEPAIRED") {
|
||||||
|
eventType = EventTypeDepaired;
|
||||||
|
} else {
|
||||||
|
qCWarning(dcHomeConnect()) << "Unhandled event type" << eventString;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
QByteArray dataLine = reply->readLine();
|
||||||
|
if (dataLine.startsWith("data")) {
|
||||||
|
data = QJsonDocument::fromJson(dataLine.remove(0,6));
|
||||||
|
|
||||||
|
QByteArray idLine = reply->readLine();
|
||||||
|
if (idLine.startsWith("id")) {
|
||||||
|
haId = idLine.split(':').last().trimmed();
|
||||||
|
} else {
|
||||||
|
qCWarning(dcHomeConnect()) << "Id line: Unexpected line" << eventTypeLine;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
qCWarning(dcHomeConnect()) << "Data Line: Unexpected line" << eventTypeLine;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
qCWarning(dcHomeConnect()) << "Event type: Unexpected line" << eventTypeLine;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.toVariant().toMap().contains("items")) {
|
||||||
|
QList<Event> events;
|
||||||
|
QVariantList itemsList = data.toVariant().toMap().value("items").toList();
|
||||||
|
Q_FOREACH(QVariant item, itemsList) {
|
||||||
|
QVariantMap map = item.toMap();
|
||||||
|
Event event;
|
||||||
|
event.key = map["key"].toString();
|
||||||
|
event.uri = map["uri"].toString();
|
||||||
|
event.name = map["uri"].toString();
|
||||||
|
event.value = map["value"];
|
||||||
|
event.unit = map["unit"].toString();
|
||||||
|
event.timestamp = map["timestamp"].toInt();
|
||||||
|
events.append(event);
|
||||||
|
}
|
||||||
|
if (!events.isEmpty())
|
||||||
|
emit receivedEvents(eventType, haId, events);
|
||||||
|
} else if (data.toVariant().toMap().contains("error")) {
|
||||||
|
qCWarning(dcHomeConnect()) << "Event stream error" << data.toVariant().toMap().value("error");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
QUuid HomeConnect::sendCommand(const QString &haid, const QString &command)
|
||||||
|
{
|
||||||
|
QUuid commandId = QUuid::createUuid();
|
||||||
|
QUrl url = QUrl(m_baseControlUrl+"/api/homeappliances/"+haid+"/commands/"+command);
|
||||||
|
|
||||||
|
QNetworkRequest request(url);
|
||||||
|
request.setRawHeader("Authorization", "Bearer "+m_accessToken);
|
||||||
|
request.setRawHeader("Accept-Language", "en-US");
|
||||||
|
request.setRawHeader("accept", "application/vnd.bsh.sdk.v1+json");
|
||||||
|
|
||||||
|
QJsonDocument doc;
|
||||||
|
QJsonObject data;
|
||||||
|
data.insert("key", command);
|
||||||
|
data.insert("value", true);
|
||||||
|
QJsonObject obj;
|
||||||
|
obj.insert("data", data);
|
||||||
|
doc.setObject(obj);
|
||||||
|
QNetworkReply *reply = m_networkManager->put(request, doc.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(dcHomeConnect()) << "Send command" << map;
|
||||||
|
if (map.contains("data")) {
|
||||||
|
QVariantMap dataMap = map.value("data").toMap();
|
||||||
|
qCDebug(dcHomeConnect()) << "key" << dataMap.value("key").toString() << "value" << dataMap.value("value").toString() << dataMap.value("unit").toString();
|
||||||
|
} else if (map.contains("error")) {
|
||||||
|
qCWarning(dcHomeConnect()) << "Send command" << map.value("error").toMap().value("description").toString();
|
||||||
|
}
|
||||||
|
emit commandExecuted(commandId, true);
|
||||||
|
});
|
||||||
|
return commandId;
|
||||||
|
}
|
||||||
|
|
||||||
|
void HomeConnect::setAuthenticated(bool state)
|
||||||
|
{
|
||||||
|
if (state != m_authenticated) {
|
||||||
|
m_authenticated = state;
|
||||||
|
emit authenticationStatusChanged(state);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void HomeConnect::setConnected(bool state)
|
||||||
|
{
|
||||||
|
if (state != m_connected) {
|
||||||
|
m_connected = state;
|
||||||
|
emit connectionChanged(state);
|
||||||
|
}
|
||||||
|
}
|
||||||
188
homeconnect/homeconnect.h
Normal file
188
homeconnect/homeconnect.h
Normal file
@ -0,0 +1,188 @@
|
|||||||
|
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
|
||||||
|
*
|
||||||
|
* 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 <https://www.gnu.org/licenses/>.
|
||||||
|
*
|
||||||
|
* 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
|
||||||
|
*
|
||||||
|
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
|
||||||
|
|
||||||
|
#ifndef HOMECONNECT_H
|
||||||
|
#define HOMECONNECT_H
|
||||||
|
|
||||||
|
#include <QObject>
|
||||||
|
#include <QTimer>
|
||||||
|
#include <QUuid>
|
||||||
|
|
||||||
|
#include "network/networkaccessmanager.h"
|
||||||
|
|
||||||
|
class HomeConnect : public QObject
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
public:
|
||||||
|
enum EventType {
|
||||||
|
EventTypeKeepAlive,
|
||||||
|
EventTypeStatus,
|
||||||
|
EventTypeEvent,
|
||||||
|
EventTypeNotify,
|
||||||
|
EventTypeDisconnected,
|
||||||
|
EventTypeConnected,
|
||||||
|
EventTypePaired,
|
||||||
|
EventTypeDepaired
|
||||||
|
};
|
||||||
|
|
||||||
|
enum Type {
|
||||||
|
Oven,
|
||||||
|
Dishwasher,
|
||||||
|
Washer,
|
||||||
|
Dryer,
|
||||||
|
WasherDryer,
|
||||||
|
FridgeFreezer,
|
||||||
|
Refrigerator,
|
||||||
|
Freezer,
|
||||||
|
WineCooler,
|
||||||
|
CoffeeMaker,
|
||||||
|
Hood,
|
||||||
|
CleaningRobot,
|
||||||
|
CookProcessor
|
||||||
|
};
|
||||||
|
|
||||||
|
struct HomeAppliance {
|
||||||
|
QString name;
|
||||||
|
QString brand;
|
||||||
|
QString enumber;
|
||||||
|
QString vib;
|
||||||
|
bool connected;
|
||||||
|
QString type;
|
||||||
|
QString homeApplianceId;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct OptionData {
|
||||||
|
QString key;
|
||||||
|
QVariant value;
|
||||||
|
QString unit;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Option {
|
||||||
|
QString key;
|
||||||
|
QVariant value;
|
||||||
|
QString unit;
|
||||||
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
"key": "Cooking.Oven.Option.SetpointTemperature",
|
||||||
|
"name": "Target temperature for the oven",
|
||||||
|
"uri": "/api/homeappliances/BOSCH-HNG6764B6-0000000011FF/programs/active/options/Cooking.Oven.Option.SetpointTemperature",
|
||||||
|
"timestamp": 1556793979,
|
||||||
|
"level": "hint",
|
||||||
|
"handling": "none",
|
||||||
|
"value": 200,
|
||||||
|
"unit": "°C"
|
||||||
|
*/
|
||||||
|
struct Event {
|
||||||
|
QString key;
|
||||||
|
QString name;
|
||||||
|
QString uri;
|
||||||
|
int timestamp;
|
||||||
|
QVariant value;
|
||||||
|
QString unit;
|
||||||
|
};
|
||||||
|
|
||||||
|
HomeConnect(NetworkAccessManager *networkmanager, const QByteArray &clientKey, const QByteArray &clientSecret, bool simulationMode = false, QObject *parent = nullptr);
|
||||||
|
QByteArray accessToken();
|
||||||
|
QByteArray refreshToken();
|
||||||
|
void setSimulationMode(bool simulation);
|
||||||
|
|
||||||
|
QUrl getLoginUrl(const QUrl &redirectUrl, const QString &scope);
|
||||||
|
|
||||||
|
void getAccessTokenFromRefreshToken(const QByteArray &refreshToken);
|
||||||
|
void getAccessTokenFromAuthorizationCode(const QByteArray &authorizationCode);
|
||||||
|
|
||||||
|
// DEFAULT
|
||||||
|
void getHomeAppliances(); // Get all home appliances which are paired with the logged-in user account.
|
||||||
|
|
||||||
|
// PROGRAMS
|
||||||
|
void getPrograms(const QString &haId); //Get all programs of a given home appliance
|
||||||
|
void getProgramsAvailable(const QString &haId); //Get all programs which are currently available on the given home appliance
|
||||||
|
void getProgramsActive(const QString &haId); //Get program which is currently executed
|
||||||
|
void getProgramsSelected(const QString &haId); //Get the program which is currently selected
|
||||||
|
void getProgramsActiveOption(const QString &haId, const QString &optionKey);
|
||||||
|
QUuid selectProgram(const QString &haId, const QString &programKey, QList<Option> options);
|
||||||
|
QUuid setSelectedProgramOptions(const QString &haId, QList<Option> options);
|
||||||
|
QUuid startProgram(const QString &haId, const QString &programKey, QList<Option> options);
|
||||||
|
QUuid stopProgram(const QString &haId);
|
||||||
|
|
||||||
|
// STATUS EVENTS
|
||||||
|
void getStatus(const QString &haid);
|
||||||
|
void connectEventStream();
|
||||||
|
|
||||||
|
// SETTINGS
|
||||||
|
void getSettings(const QString &haid);
|
||||||
|
|
||||||
|
// COMMANDS
|
||||||
|
QUuid sendCommand(const QString &haid, const QString &command); //commands "BSH.Common.Command.ResumeProgram" & "PauseProgram"
|
||||||
|
|
||||||
|
private:
|
||||||
|
bool m_simulationMode = false;
|
||||||
|
QByteArray m_baseAuthorizationUrl;
|
||||||
|
QByteArray m_baseTokenUrl;
|
||||||
|
QByteArray m_baseControlUrl;
|
||||||
|
QByteArray m_clientKey;
|
||||||
|
QByteArray m_clientSecret;
|
||||||
|
|
||||||
|
QByteArray m_accessToken;
|
||||||
|
QByteArray m_refreshToken;
|
||||||
|
QByteArray m_redirectUri = "https://127.0.0.1:8888";
|
||||||
|
QString m_codeChallenge;
|
||||||
|
|
||||||
|
NetworkAccessManager *m_networkManager = nullptr;
|
||||||
|
QTimer *m_tokenRefreshTimer = nullptr;
|
||||||
|
|
||||||
|
void setAuthenticated(bool state);
|
||||||
|
void setConnected(bool state);
|
||||||
|
|
||||||
|
bool m_authenticated = false;
|
||||||
|
bool m_connected = false;
|
||||||
|
|
||||||
|
bool checkStatusCode(QNetworkReply *reply, const QByteArray &rawData);
|
||||||
|
private slots:
|
||||||
|
void onRefreshTimeout();
|
||||||
|
|
||||||
|
signals:
|
||||||
|
void connectionChanged(bool connected);
|
||||||
|
void authenticationStatusChanged(bool authenticated);
|
||||||
|
void receivedRefreshToken(const QByteArray &refreshToken);
|
||||||
|
void receivedAccessToken(const QByteArray &accessToken);
|
||||||
|
void commandExecuted(const QUuid &commandId, bool success);
|
||||||
|
|
||||||
|
void receivedHomeAppliances(const QList<HomeAppliance> &appliances);
|
||||||
|
void receivedStatusList(const QString &haId, const QHash<QString, QVariant> &statusList);
|
||||||
|
void receivedPrograms(const QString &haId, const QStringList &programs);
|
||||||
|
void receivedAvailablePrograms(const QString &haId, const QStringList &programs);
|
||||||
|
void receivedSettings(const QString &haId, const QHash<QString, QVariant> &settings);
|
||||||
|
void receivedActiveProgram(const QString &haId, const QString &key, const QHash<QString, QVariant> &options);
|
||||||
|
void receivedSelectedProgram(const QString &haId, const QString &key, const QHash<QString, QVariant> &options);
|
||||||
|
void receivedEvents(EventType eventType, const QString &haId, const QList<Event> &events);
|
||||||
|
};
|
||||||
|
#endif // HOMECONNECT_H
|
||||||
BIN
homeconnect/homeconnect.png
Normal file
BIN
homeconnect/homeconnect.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 189 KiB |
11
homeconnect/homeconnect.pro
Normal file
11
homeconnect/homeconnect.pro
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
include(../plugins.pri)
|
||||||
|
|
||||||
|
QT += network
|
||||||
|
|
||||||
|
SOURCES += \
|
||||||
|
integrationpluginhomeconnect.cpp \
|
||||||
|
homeconnect.cpp \
|
||||||
|
|
||||||
|
HEADERS += \
|
||||||
|
integrationpluginhomeconnect.h \
|
||||||
|
homeconnect.h \
|
||||||
1085
homeconnect/integrationpluginhomeconnect.cpp
Normal file
1085
homeconnect/integrationpluginhomeconnect.cpp
Normal file
File diff suppressed because it is too large
Load Diff
108
homeconnect/integrationpluginhomeconnect.h
Normal file
108
homeconnect/integrationpluginhomeconnect.h
Normal file
@ -0,0 +1,108 @@
|
|||||||
|
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
|
||||||
|
*
|
||||||
|
* 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 <https://www.gnu.org/licenses/>.
|
||||||
|
*
|
||||||
|
* 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
|
||||||
|
*
|
||||||
|
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
|
||||||
|
|
||||||
|
#ifndef INTEGRATIONPLUGINHOMECONNECT_H
|
||||||
|
#define INTEGRATIONPLUGINHOMECONNECT_H
|
||||||
|
|
||||||
|
#include "integrations/integrationplugin.h"
|
||||||
|
#include "plugintimer.h"
|
||||||
|
#include "homeconnect.h"
|
||||||
|
|
||||||
|
#include <QHash>
|
||||||
|
#include <QDebug>
|
||||||
|
|
||||||
|
class IntegrationPluginHomeConnect : public IntegrationPlugin
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
Q_PLUGIN_METADATA(IID "io.nymea.IntegrationPlugin" FILE "integrationpluginhomeconnect.json")
|
||||||
|
Q_INTERFACES(IntegrationPlugin)
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit IntegrationPluginHomeConnect();
|
||||||
|
|
||||||
|
void startPairing(ThingPairingInfo *info) override;
|
||||||
|
void confirmPairing(ThingPairingInfo *info, const QString &username, const QString &secret) override;
|
||||||
|
|
||||||
|
void setupThing(ThingSetupInfo *info) override;
|
||||||
|
void postSetupThing(Thing *thing) override;
|
||||||
|
void executeAction(ThingActionInfo *info) override;
|
||||||
|
void thingRemoved(Thing *thing) override;
|
||||||
|
|
||||||
|
void browseThing(BrowseResult *result) override;
|
||||||
|
void browserItem(BrowserItemResult *result) override;
|
||||||
|
void executeBrowserItem(BrowserActionInfo *info) override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
PluginTimer *m_pluginTimer15min = nullptr;
|
||||||
|
|
||||||
|
QHash<HomeConnect *, ThingSetupInfo *> m_asyncSetup;
|
||||||
|
|
||||||
|
QHash<ThingId, HomeConnect *> m_setupHomeConnectConnections;
|
||||||
|
QHash<Thing *, HomeConnect *> m_homeConnectConnections;
|
||||||
|
|
||||||
|
QHash<QUuid, ThingActionInfo *> m_pendingActions;
|
||||||
|
QHash<Thing *, QString> m_selectedProgram;
|
||||||
|
|
||||||
|
QHash<ThingClassId, ParamTypeId> m_idParamTypeIds;
|
||||||
|
|
||||||
|
QHash<ThingClassId, StateTypeId> m_connectedStateTypeIds;
|
||||||
|
QHash<ThingClassId, StateTypeId> m_doorStateStateTypeIds;
|
||||||
|
QHash<ThingClassId, StateTypeId> m_localControlStateTypeIds;
|
||||||
|
QHash<ThingClassId, StateTypeId> m_remoteControlActivationStateTypeIds;
|
||||||
|
QHash<ThingClassId, StateTypeId> m_remoteStartAllowanceStateTypeIds;
|
||||||
|
QHash<ThingClassId, StateTypeId> m_operationStateTypeIds;
|
||||||
|
QHash<ThingClassId, StateTypeId> m_doorStateTypeIds;
|
||||||
|
QHash<ThingClassId, StateTypeId> m_selectedProgramStateTypeIds;
|
||||||
|
QHash<ThingClassId, StateTypeId> m_progressStateTypeIds;
|
||||||
|
QHash<ThingClassId, StateTypeId> m_endTimerStateTypeIds;
|
||||||
|
|
||||||
|
QHash<ThingClassId, ActionTypeId> m_startActionTypeIds;
|
||||||
|
QHash<ThingClassId, ActionTypeId> m_stopActionTypeIds;
|
||||||
|
|
||||||
|
QHash<ThingClassId, EventTypeId> m_programFinishedEventTypeIds;
|
||||||
|
|
||||||
|
QHash<QString, QString> m_coffeeStrengthTypes;
|
||||||
|
|
||||||
|
void parseKey(Thing *thing, const QString &key, const QVariant &value);
|
||||||
|
void parseSettingKey(Thing *thing, const QString &key, const QVariant &value);
|
||||||
|
bool checkIfActionIsPossible(ThingActionInfo *info);
|
||||||
|
|
||||||
|
private slots:
|
||||||
|
void onConnectionChanged(bool connected);
|
||||||
|
void onAuthenticationStatusChanged(bool authenticated);
|
||||||
|
void onRequestExecuted(QUuid requestId, bool success);
|
||||||
|
void onReceivedHomeAppliances(const QList<HomeConnect::HomeAppliance> &appliances);
|
||||||
|
void onReceivedStatusList(const QString &haId, const QHash<QString, QVariant> &statusList);
|
||||||
|
void onReceivedEvents(HomeConnect::EventType eventType, const QString &haId, const QList<HomeConnect::Event> &events);
|
||||||
|
void onReceivedSelectedProgram(const QString &haId, const QString &key, const QHash<QString, QVariant> &options);
|
||||||
|
void onReceivedSettings(const QString &haId, const QHash<QString, QVariant> &settings);
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif // INTEGRATIONPLUGINHOMECONNECT_H
|
||||||
1112
homeconnect/integrationpluginhomeconnect.json
Normal file
1112
homeconnect/integrationpluginhomeconnect.json
Normal file
File diff suppressed because it is too large
Load Diff
13
homeconnect/meta.json
Normal file
13
homeconnect/meta.json
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"title": "Home Connect",
|
||||||
|
"tagline": "Connect to Home Connect devices.",
|
||||||
|
"icon": "homeconnect.png",
|
||||||
|
"stability": "consumer",
|
||||||
|
"offline": false,
|
||||||
|
"technologies": [
|
||||||
|
"network"
|
||||||
|
],
|
||||||
|
"categories": [
|
||||||
|
"online-service"
|
||||||
|
]
|
||||||
|
}
|
||||||
1073
homeconnect/translations/109abdc7-5d53-4f63-a4b2-851e97cea8ea-de.ts
Normal file
1073
homeconnect/translations/109abdc7-5d53-4f63-a4b2-851e97cea8ea-de.ts
Normal file
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -26,6 +26,7 @@ PLUGIN_DIRS = \
|
|||||||
gpio \
|
gpio \
|
||||||
i2cdevices \
|
i2cdevices \
|
||||||
httpcommander \
|
httpcommander \
|
||||||
|
homeconnect \
|
||||||
keba \
|
keba \
|
||||||
kodi \
|
kodi \
|
||||||
lgsmarttv \
|
lgsmarttv \
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user