remote connection support done
This commit is contained in:
parent
e9390dcb5a
commit
ce06ffbcce
@ -11,11 +11,10 @@
|
||||
#include "qmqtt.h"
|
||||
#include "sigv4utils.h"
|
||||
|
||||
static QByteArray clientId = "8rjhfdlf9jf1suok2jcrltd6v";
|
||||
static QByteArray region = "eu-west-1";
|
||||
//static QByteArray service = "iotdevicegateway";
|
||||
static QByteArray service = "iotdata";
|
||||
|
||||
// This is Symantec's root CA certificate and most platforms should
|
||||
// have this in their certificate storage already, but as we can't
|
||||
// be certain about the core's setup, let's deploy it ourselves.
|
||||
static QByteArray rootCA = "-----BEGIN CERTIFICATE-----\n\
|
||||
MIIE0zCCA7ugAwIBAgIQGNrRniZ96LtKIVjNzGs7SjANBgkqhkiG9w0BAQUFADCB\n\
|
||||
yjELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDlZlcmlTaWduLCBJbmMuMR8wHQYDVQQL\n\
|
||||
@ -51,6 +50,31 @@ AWSClient::AWSClient(QObject *parent) : QObject(parent),
|
||||
{
|
||||
m_nam = new QNetworkAccessManager(this);
|
||||
|
||||
AWSConfiguration config;
|
||||
// Community environment
|
||||
config.clientId = "35duli0b13c7pet5k4bcv8pbu";
|
||||
config.poolId = "eu-west-1_WZVsaBsaY";
|
||||
config.identityPoolId = "eu-west-1:17449947-1a2f-4dda-aa49-7c5b1eec78d7";
|
||||
config.certificateEndpoint = "https://communityservice-cloud.nymea.io/certificatews/certificate";
|
||||
config.certificateApiKey = "aIRQv4yDdF6ASq12X1CPp7b6MpkdODfI3AOjOnkE";
|
||||
config.certificateVendorId = "d399290a-0599-4895-b4c3-34d2bdb579f4";
|
||||
config.mqttEndpoint = "a2d0ba9572wepp.iot.eu-west-1.amazonaws.com";
|
||||
config.region = "eu-west-1";
|
||||
config.apiEndpoint = "api-cloud.guh.io";
|
||||
m_configs.append(config);
|
||||
|
||||
// Testing environment
|
||||
config.clientId = "8rjhfdlf9jf1suok2jcrltd6v";
|
||||
config.poolId = "eu-west-1_6eX6YjmXr";
|
||||
config.identityPoolId = "eu-west-1:108a174c-5786-40f9-966a-1a0cd33d6801";
|
||||
config.certificateEndpoint = "https://testcommunityservice-cloud.nymea.io/certificatews/certificate";
|
||||
config.certificateApiKey = "VhmAUy75eZ9jXaUEjgWZh9PpSIykPGBK7AZFPimh";
|
||||
config.certificateVendorId = "testVendor001";
|
||||
config.mqttEndpoint = "a2addxakg5juii.iot.eu-west-1.amazonaws.com";
|
||||
config.region = "eu-west-1";
|
||||
config.apiEndpoint = "testapi-cloud.guh.io";
|
||||
m_configs.append(config);
|
||||
|
||||
QSettings settings;
|
||||
settings.beginGroup("cloud");
|
||||
m_username = settings.value("username").toString();
|
||||
@ -59,6 +83,7 @@ AWSClient::AWSClient(QObject *parent) : QObject(parent),
|
||||
m_accessTokenExpiry = settings.value("accessTokenExpiry").toDateTime();
|
||||
m_idToken = settings.value("idToken").toByteArray();
|
||||
m_refreshToken = settings.value("refreshToken").toByteArray();
|
||||
m_confirmationPending = settings.value("confirmationPending", false).toBool();
|
||||
|
||||
m_identityId = settings.value("identityId").toByteArray();
|
||||
|
||||
@ -88,16 +113,22 @@ AWSDevices *AWSClient::awsDevices() const
|
||||
return m_devices;
|
||||
}
|
||||
|
||||
bool AWSClient::confirmationPending() const
|
||||
{
|
||||
return m_confirmationPending;
|
||||
}
|
||||
|
||||
void AWSClient::login(const QString &username, const QString &password)
|
||||
{
|
||||
m_username = username;
|
||||
// Ok... Please fogive me for this... AWS APIs are just unbearable... can't be bothered
|
||||
// any more to walk through another chain of calls in order to have the refreshToken working.
|
||||
// Due to an issue in AWS apis it's very complex to use the refresh token. Taking a shortcut here for now:
|
||||
// Will store the password in the config for now and re-login when the accessToken expires.
|
||||
// See: https://forums.aws.amazon.com/thread.jspa?threadID=287978
|
||||
// Ideally we'd use the refresh token and not store the password at all (see: refreshAccessToken())
|
||||
m_password = password;
|
||||
|
||||
QUrl url("https://cognito-idp.eu-west-1.amazonaws.com/");
|
||||
QString host = QString("cognito-idp.%1.amazonaws.com").arg(m_configs.at(m_usedConfigIndex).region);
|
||||
QUrl url(QString("https://%1/").arg(host));
|
||||
|
||||
QUrlQuery query;
|
||||
query.addQueryItem("Action", "InitiateAuth");
|
||||
@ -106,12 +137,12 @@ void AWSClient::login(const QString &username, const QString &password)
|
||||
|
||||
QNetworkRequest request(url);
|
||||
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-amz-json-1.0");
|
||||
request.setRawHeader("Host", "cognito-idp.eu-west-1.amazonaws.com");
|
||||
request.setRawHeader("Host", host.toUtf8());
|
||||
request.setRawHeader("X-Amz-Target", "AWSCognitoIdentityProviderService.InitiateAuth");
|
||||
|
||||
QVariantMap params;
|
||||
params.insert("AuthFlow", "USER_PASSWORD_AUTH");
|
||||
params.insert("ClientId", clientId);
|
||||
params.insert("ClientId", m_configs.at(m_usedConfigIndex).clientId);
|
||||
|
||||
QVariantMap authParams;
|
||||
authParams.insert("USERNAME", username);
|
||||
@ -131,6 +162,7 @@ void AWSClient::login(const QString &username, const QString &password)
|
||||
qWarning() << "Error logging in to aws:" << reply->error() << reply->errorString();
|
||||
m_username.clear();
|
||||
m_password.clear();
|
||||
emit loginResult(LoginErrorUnknownError);
|
||||
return;
|
||||
}
|
||||
QByteArray data = reply->readAll();
|
||||
@ -140,6 +172,7 @@ void AWSClient::login(const QString &username, const QString &password)
|
||||
qWarning() << "Failed to parse AWS login response" << error.errorString();
|
||||
m_username.clear();
|
||||
m_password.clear();
|
||||
emit loginResult(LoginErrorUnknownError);
|
||||
return;
|
||||
}
|
||||
|
||||
@ -160,7 +193,7 @@ void AWSClient::login(const QString &username, const QString &password)
|
||||
settings.setValue("idToken", m_idToken);
|
||||
settings.setValue("refreshToken", m_refreshToken);
|
||||
|
||||
qDebug() << "AWS login successful" << qUtf8Printable(jsonDoc.toJson(QJsonDocument::Indented));
|
||||
qDebug() << "AWS login successful";// << qUtf8Printable(jsonDoc.toJson(QJsonDocument::Indented));
|
||||
emit isLoggedInChanged();
|
||||
|
||||
qDebug() << "Getting cognito ID";
|
||||
@ -177,9 +210,310 @@ void AWSClient::logout()
|
||||
emit isLoggedInChanged();
|
||||
}
|
||||
|
||||
void AWSClient::signup(const QString &username, const QString &password)
|
||||
{
|
||||
m_userId = QUuid::createUuid().toString().remove(QRegExp("[{}]"));
|
||||
m_username = username;
|
||||
m_password = password;
|
||||
|
||||
QString host = QString("cognito-idp.%1.amazonaws.com").arg(m_configs.at(m_usedConfigIndex).region);
|
||||
QUrl url(QString("https://%1/").arg(host));
|
||||
|
||||
QUrlQuery query;
|
||||
query.addQueryItem("Action", "SignUp");
|
||||
query.addQueryItem("Version", "2016-04-18");
|
||||
url.setQuery(query);
|
||||
|
||||
QNetworkRequest request(url);
|
||||
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-amz-json-1.0");
|
||||
request.setRawHeader("Host", host.toUtf8());
|
||||
request.setRawHeader("X-Amz-Target", "AWSCognitoIdentityProviderService.SignUp");
|
||||
|
||||
QVariantMap params;
|
||||
params.insert("ClientId", m_configs.at(m_usedConfigIndex).clientId);
|
||||
params.insert("Username", m_userId);
|
||||
params.insert("Password", password);
|
||||
|
||||
QVariantMap emailAttribute;
|
||||
emailAttribute.insert("Name", "email");
|
||||
emailAttribute.insert("Value", username);
|
||||
|
||||
QVariantList userAttributes;
|
||||
userAttributes.append(emailAttribute);
|
||||
params.insert("UserAttributes", userAttributes);
|
||||
|
||||
QJsonDocument jsonDoc = QJsonDocument::fromVariant(params);
|
||||
QByteArray payload = jsonDoc.toJson(QJsonDocument::Compact);
|
||||
|
||||
qDebug() << "Signing up to AWS as user:" << username << payload;
|
||||
|
||||
QNetworkReply *reply = m_nam->post(request, payload);
|
||||
connect(reply, &QNetworkReply::finished, this, [this, reply]() {
|
||||
QByteArray data = reply->readAll();
|
||||
reply->deleteLater();
|
||||
qDebug() << "AWS signup reply:" << data;
|
||||
|
||||
if (reply->error() == QNetworkReply::ProtocolInvalidOperationError) {
|
||||
emit signupResult(LoginErrorInvalidUserOrPass);
|
||||
return;
|
||||
}
|
||||
|
||||
if (reply->error() != QNetworkReply::NoError) {
|
||||
qWarning() << "Error signing up to aws:" << reply->error() << reply->errorString();
|
||||
m_username.clear();
|
||||
m_password.clear();
|
||||
emit signupResult(LoginErrorUnknownError);
|
||||
return;
|
||||
}
|
||||
|
||||
emit signupResult(LoginErrorNoError);
|
||||
|
||||
QSettings settings;
|
||||
settings.beginGroup("cloud");
|
||||
settings.setValue("username", m_username);
|
||||
settings.setValue("password", m_password);
|
||||
settings.setValue("confirmationPending", true);
|
||||
|
||||
m_confirmationPending = true;
|
||||
emit confirmationPendingChanged();
|
||||
});
|
||||
}
|
||||
|
||||
void AWSClient::confirmRegistration(const QString &code)
|
||||
{
|
||||
QString host = QString("cognito-idp.%1.amazonaws.com").arg(m_configs.at(m_usedConfigIndex).region);
|
||||
QUrl url(QString("https://%1/").arg(host));
|
||||
|
||||
QUrlQuery query;
|
||||
query.addQueryItem("Action", "ConfirmSignUp");
|
||||
query.addQueryItem("Version", "2016-04-18");
|
||||
url.setQuery(query);
|
||||
|
||||
QNetworkRequest request(url);
|
||||
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-amz-json-1.0");
|
||||
request.setRawHeader("Host", host.toUtf8());
|
||||
request.setRawHeader("X-Amz-Target", "AWSCognitoIdentityProviderService.ConfirmSignUp");
|
||||
|
||||
QVariantMap params;
|
||||
params.insert("ClientId", m_configs.at(m_usedConfigIndex).clientId);
|
||||
params.insert("Username", m_userId);
|
||||
params.insert("ConfirmationCode", code);
|
||||
|
||||
QJsonDocument jsonDoc = QJsonDocument::fromVariant(params);
|
||||
QByteArray payload = jsonDoc.toJson(QJsonDocument::Compact);
|
||||
|
||||
qDebug() << "Confirming registration for user:" << m_username;
|
||||
|
||||
QNetworkReply *reply = m_nam->post(request, payload);
|
||||
connect(reply, &QNetworkReply::finished, this, [this, reply]() {
|
||||
QByteArray data = reply->readAll();
|
||||
reply->deleteLater();
|
||||
qDebug() << "AWS signup reply:" << data;
|
||||
|
||||
if (reply->error() == QNetworkReply::ProtocolInvalidOperationError) {
|
||||
QJsonParseError error;
|
||||
QVariantMap result = QJsonDocument::fromJson(data, &error).toVariant().toMap();
|
||||
if (result.value("__type").toString() == "com.amazonaws.cognito.identity.idp.model#CodeMismatchException") {
|
||||
emit confirmationResult(LoginErrorInvalidCode);
|
||||
return;
|
||||
} else if (result.value("__type").toString() == "com.amazonaws.cognito.identity.idp.model#AliasExistsException") {
|
||||
emit confirmationResult(LoginErrorUserExists);
|
||||
return;
|
||||
}
|
||||
emit confirmationResult(LoginErrorUnknownError);
|
||||
return;
|
||||
}
|
||||
|
||||
if (reply->error() != QNetworkReply::NoError) {
|
||||
qWarning() << "Error confirming registration:" << reply->error() << reply->errorString();
|
||||
emit confirmationResult(LoginErrorUnknownError);
|
||||
return;
|
||||
}
|
||||
|
||||
emit confirmationResult(LoginErrorNoError);
|
||||
login(m_username, m_password);
|
||||
fetchDevices();
|
||||
});
|
||||
}
|
||||
|
||||
void AWSClient::forgotPassword(const QString &username)
|
||||
{
|
||||
QString host = QString("cognito-idp.%1.amazonaws.com").arg(m_configs.at(m_usedConfigIndex).region);
|
||||
QUrl url(QString("https://%1/").arg(host));
|
||||
|
||||
QUrlQuery query;
|
||||
query.addQueryItem("Action", "ForgotPassword");
|
||||
query.addQueryItem("Version", "2016-04-18");
|
||||
url.setQuery(query);
|
||||
|
||||
QNetworkRequest request(url);
|
||||
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-amz-json-1.0");
|
||||
request.setRawHeader("Host", host.toUtf8());
|
||||
request.setRawHeader("X-Amz-Target", "AWSCognitoIdentityProviderService.ForgotPassword");
|
||||
|
||||
QVariantMap params;
|
||||
params.insert("ClientId", m_configs.at(m_usedConfigIndex).clientId);
|
||||
params.insert("Username", username);
|
||||
|
||||
QJsonDocument jsonDoc = QJsonDocument::fromVariant(params);
|
||||
QByteArray payload = jsonDoc.toJson(QJsonDocument::Compact);
|
||||
|
||||
qDebug() << "Forgot password for user:" << username << payload;
|
||||
|
||||
QNetworkReply *reply = m_nam->post(request, payload);
|
||||
connect(reply, &QNetworkReply::finished, this, [this, reply]() {
|
||||
QByteArray data = reply->readAll();
|
||||
reply->deleteLater();
|
||||
|
||||
if (reply->error() == QNetworkReply::ProtocolInvalidOperationError) {
|
||||
QJsonDocument jsonDoc = QJsonDocument::fromJson(data);
|
||||
if (jsonDoc.toVariant().toMap().value("__type").toString() == "com.amazonaws.cognito.identity.idp.model#LimitExceededException") {
|
||||
emit forgotPasswordResult(LoginErrorLimitExceeded);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (reply->error() != QNetworkReply::NoError) {
|
||||
qWarning() << "Error calling ForgotPassword:" << reply->error() << reply->errorString() << data;
|
||||
emit forgotPasswordResult(LoginErrorUnknownError);
|
||||
return;
|
||||
}
|
||||
|
||||
qDebug() << "AWS forgotPassword success:" << data;
|
||||
emit forgotPasswordResult(LoginErrorNoError);
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
void AWSClient::confirmForgotPassword(const QString &username, const QString &code, const QString &newPassword)
|
||||
{
|
||||
QString host = QString("cognito-idp.%1.amazonaws.com").arg(m_configs.at(m_usedConfigIndex).region);
|
||||
QUrl url(QString("https://%1/").arg(host));
|
||||
|
||||
QUrlQuery query;
|
||||
query.addQueryItem("Action", "ConfirmForgotPassword");
|
||||
query.addQueryItem("Version", "2016-04-18");
|
||||
url.setQuery(query);
|
||||
|
||||
QNetworkRequest request(url);
|
||||
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-amz-json-1.0");
|
||||
request.setRawHeader("Host", host.toUtf8());
|
||||
request.setRawHeader("X-Amz-Target", "AWSCognitoIdentityProviderService.ConfirmForgotPassword");
|
||||
|
||||
QVariantMap params;
|
||||
params.insert("ClientId", m_configs.at(m_usedConfigIndex).clientId);
|
||||
params.insert("ConfirmationCode", code);
|
||||
params.insert("Username", username);
|
||||
params.insert("Password", newPassword);
|
||||
|
||||
QJsonDocument jsonDoc = QJsonDocument::fromVariant(params);
|
||||
QByteArray payload = jsonDoc.toJson(QJsonDocument::Compact);
|
||||
|
||||
qDebug() << "ConfirmForgotPassword for user:" << username << payload;
|
||||
|
||||
QNetworkReply *reply = m_nam->post(request, payload);
|
||||
connect(reply, &QNetworkReply::finished, this, [this, reply]() {
|
||||
QByteArray data = reply->readAll();
|
||||
reply->deleteLater();
|
||||
|
||||
if (reply->error() != QNetworkReply::NoError) {
|
||||
qWarning() << "Error calling ConfirmForgotPassword:" << reply->error() << reply->errorString() << data;
|
||||
emit confirmForgotPasswordResult(LoginErrorUnknownError);
|
||||
return;
|
||||
}
|
||||
|
||||
qDebug() << "AWS ConfirmForgotPassword success:" << data;
|
||||
emit confirmForgotPasswordResult(LoginErrorNoError);
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
void AWSClient::deleteAccount()
|
||||
{
|
||||
if (!isLoggedIn()) {
|
||||
qWarning() << "Not logged in at AWS. Can't delete account";
|
||||
return;
|
||||
}
|
||||
if (tokensExpired()) {
|
||||
qDebug() << "Cannot unpair device. Need to refresh our tokens";
|
||||
refreshAccessToken();
|
||||
m_callQueue.append(QueuedCall("deleteAccount"));
|
||||
return;
|
||||
}
|
||||
qDebug() << "Deleting account";
|
||||
|
||||
QUrl url(QString("https://%1/users/profiles/%2").arg(m_configs.at(m_usedConfigIndex).apiEndpoint).arg(m_username));
|
||||
QNetworkRequest request(url);
|
||||
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
|
||||
request.setRawHeader("x-api-idToken", m_idToken);
|
||||
|
||||
qDebug() << "DELETE" << url.toString();
|
||||
qDebug() << "HEADERS:";
|
||||
foreach (const QByteArray &hdr, request.rawHeaderList()) {
|
||||
qDebug() << hdr << ":" << request.rawHeader(hdr);
|
||||
}
|
||||
|
||||
QNetworkReply *reply = m_nam->deleteResource(request);
|
||||
connect(reply, &QNetworkReply::finished, this, [this, reply]() {
|
||||
reply->deleteLater();
|
||||
QByteArray data = reply->readAll();
|
||||
if (reply->error() != QNetworkReply::NoError) {
|
||||
qWarning() << "Error deleting cloud user account:" << reply->error() << reply->errorString() << qUtf8Printable(data);
|
||||
return;
|
||||
}
|
||||
QJsonParseError error;
|
||||
QJsonDocument jsonDoc = QJsonDocument::fromJson(data, &error);
|
||||
if (error.error != QJsonParseError::NoError) {
|
||||
qWarning() << "Failed to parse JSON from server" << error.errorString() << qUtf8Printable(data);
|
||||
return;
|
||||
}
|
||||
logout();
|
||||
qDebug() << "Account deleted" << data;
|
||||
});
|
||||
}
|
||||
|
||||
void AWSClient::unpairDevice(const QString &boxId)
|
||||
{
|
||||
if (!isLoggedIn()) {
|
||||
qWarning() << "Not logged in at AWS. Can't unpair device";
|
||||
return;
|
||||
}
|
||||
if (tokensExpired()) {
|
||||
qDebug() << "Cannot unpair device. Need to refresh our tokens";
|
||||
refreshAccessToken();
|
||||
m_callQueue.append(QueuedCall("unpairDevice", boxId));
|
||||
return;
|
||||
}
|
||||
qDebug() << "unpairing device";
|
||||
QUrl url(QString("https://%1/users/devices/%2").arg(m_configs.at(m_usedConfigIndex).apiEndpoint).arg(boxId));
|
||||
QNetworkRequest request(url);
|
||||
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
|
||||
request.setRawHeader("x-api-idToken", m_idToken);
|
||||
|
||||
QNetworkReply *reply = m_nam->deleteResource(request);
|
||||
connect(reply, &QNetworkReply::finished, this, [this, reply, boxId]() {
|
||||
reply->deleteLater();
|
||||
QByteArray data = reply->readAll();
|
||||
if (reply->error() != QNetworkReply::NoError) {
|
||||
qWarning() << "Error unpairing cloud device:" << reply->error() << reply->errorString() << qUtf8Printable(data);
|
||||
return;
|
||||
}
|
||||
QJsonParseError error;
|
||||
QJsonDocument jsonDoc = QJsonDocument::fromJson(data, &error);
|
||||
if (error.error != QJsonParseError::NoError) {
|
||||
qWarning() << "Failed to parse JSON from server" << error.errorString() << qUtf8Printable(data);
|
||||
return;
|
||||
}
|
||||
qDebug() << "Device unpaired" << data;
|
||||
m_devices->remove(boxId);
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
void AWSClient::getId()
|
||||
{
|
||||
QUrl url("https://cognito-identity.eu-west-1.amazonaws.com/");
|
||||
QString host = QString("cognito-identity.%1.amazonaws.com").arg(m_configs.at(m_usedConfigIndex).region);
|
||||
QUrl url(QString("https://%1/").arg(host));
|
||||
|
||||
QUrlQuery query;
|
||||
query.addQueryItem("Action", "GetId");
|
||||
@ -188,19 +522,21 @@ void AWSClient::getId()
|
||||
|
||||
QNetworkRequest request(url);
|
||||
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-amz-json-1.0");
|
||||
request.setRawHeader("Host", "cognito-identity.eu-west-1.amazonaws.com");
|
||||
request.setRawHeader("Host", host.toUtf8());
|
||||
request.setRawHeader("X-Amz-Target", "AWSCognitoIdentityService.GetId");
|
||||
|
||||
QVariantMap logins;
|
||||
logins.insert("cognito-idp.eu-west-1.amazonaws.com/eu-west-1_6eX6YjmXr", m_idToken);
|
||||
logins.insert(QString("cognito-idp.%1.amazonaws.com/%2").arg(m_configs.at(m_usedConfigIndex).region).arg(m_configs.at(m_usedConfigIndex).poolId).toUtf8(), m_idToken);
|
||||
|
||||
QVariantMap params;
|
||||
params.insert("IdentityPoolId", "eu-west-1:108a174c-5786-40f9-966a-1a0cd33d6801");
|
||||
params.insert("IdentityPoolId", m_configs.at(m_usedConfigIndex).identityPoolId.toUtf8());
|
||||
params.insert("Logins", logins);
|
||||
|
||||
QJsonDocument jsonDoc = QJsonDocument::fromVariant(params);
|
||||
QByteArray payload = jsonDoc.toJson(QJsonDocument::Compact);
|
||||
|
||||
// qDebug() << "Posting:" << request.url().toString();
|
||||
// qDebug() << "Payload:" << payload;
|
||||
QNetworkReply *reply = m_nam->post(request, payload);
|
||||
connect(reply, &QNetworkReply::finished, this, [this, reply]() {
|
||||
reply->deleteLater();
|
||||
@ -220,7 +556,7 @@ void AWSClient::getId()
|
||||
settings.beginGroup("cloud");
|
||||
settings.setValue("identityId", m_identityId);
|
||||
|
||||
qDebug() << "Received cognito identity id" << m_identityId;
|
||||
qDebug() << "Received cognito identity id" << m_identityId;// << qUtf8Printable(data);
|
||||
getCredentialsForIdentity(m_identityId);
|
||||
|
||||
});
|
||||
@ -240,13 +576,13 @@ void AWSClient::fetchCertificate(const QString &uuid, std::function<void(const Q
|
||||
{
|
||||
QString fixedUuid = uuid;
|
||||
fixedUuid.remove(QRegExp("[{}]"));
|
||||
QNetworkRequest request(QUrl("https://testproductionservice-cloud.guh.io/certificatews/certificate"));
|
||||
request.setRawHeader("X-api-key", "BJMq4h19dB5yjVKwagTvk9u72FqLecEoWPJIkyDj");
|
||||
request.setRawHeader("X-api-vendorId", "testVendor001");
|
||||
QNetworkRequest request(m_configs.at(m_usedConfigIndex).certificateEndpoint);
|
||||
request.setRawHeader("X-api-key", m_configs.at(m_usedConfigIndex).certificateApiKey.toUtf8());
|
||||
request.setRawHeader("X-api-vendorId", m_configs.at(m_usedConfigIndex).certificateVendorId.toUtf8());
|
||||
request.setRawHeader("X-api-deviceId", fixedUuid.toUtf8());
|
||||
request.setRawHeader("X-api-serialId", "69696969");
|
||||
QNetworkReply *reply = m_nam->get(request);
|
||||
connect(reply, &QNetworkReply::finished, this, [reply, callback]() {
|
||||
connect(reply, &QNetworkReply::finished, this, [this, reply, callback]() {
|
||||
reply->deleteLater();
|
||||
QByteArray data = reply->readAll();
|
||||
if (reply->error() != QNetworkReply::NoError) {
|
||||
@ -266,14 +602,29 @@ void AWSClient::fetchCertificate(const QString &uuid, std::function<void(const Q
|
||||
qDebug() << "Certificate received" << certificate;
|
||||
qDebug() << "Public key" << publicKey;
|
||||
qDebug() << "Private key" << privateKey;
|
||||
callback(rootCA, certificate, publicKey, privateKey, "a2addxakg5juii.iot.eu-west-1.amazonaws.com");
|
||||
callback(rootCA, certificate, publicKey, privateKey, m_configs.at(m_usedConfigIndex).mqttEndpoint);
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
int AWSClient::config() const
|
||||
{
|
||||
return m_usedConfigIndex;
|
||||
}
|
||||
|
||||
void AWSClient::setConfig(int index)
|
||||
{
|
||||
if (m_usedConfigIndex != index) {
|
||||
qDebug() << "Setting AWS configuration to" << index;
|
||||
m_usedConfigIndex = index;
|
||||
emit configChanged();
|
||||
}
|
||||
}
|
||||
|
||||
void AWSClient::getCredentialsForIdentity(const QString &identityId)
|
||||
{
|
||||
QUrl url("https://cognito-identity.eu-west-1.amazonaws.com/");
|
||||
QString host = QString("cognito-identity.%1.amazonaws.com").arg(m_configs.at(m_usedConfigIndex).region);
|
||||
QUrl url(QString("https://%1/").arg(host));
|
||||
|
||||
QUrlQuery query;
|
||||
query.addQueryItem("Action", "GetCredentialsForIdentity");
|
||||
@ -282,11 +633,11 @@ void AWSClient::getCredentialsForIdentity(const QString &identityId)
|
||||
|
||||
QNetworkRequest request(url);
|
||||
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-amz-json-1.0");
|
||||
request.setRawHeader("Host", "cognito-identity.eu-west-1.amazonaws.com");
|
||||
request.setRawHeader("Host", host.toUtf8());
|
||||
request.setRawHeader("X-Amz-Target", "AWSCognitoIdentityService.GetCredentialsForIdentity");
|
||||
|
||||
QVariantMap logins;
|
||||
logins.insert("cognito-idp.eu-west-1.amazonaws.com/eu-west-1_6eX6YjmXr", m_idToken);
|
||||
logins.insert(QString("cognito-idp.eu-west-1.amazonaws.com/%1").arg(m_configs.at(m_usedConfigIndex).poolId), m_idToken);
|
||||
|
||||
QVariantMap params;
|
||||
params.insert("IdentityId", identityId);
|
||||
@ -295,18 +646,19 @@ void AWSClient::getCredentialsForIdentity(const QString &identityId)
|
||||
QJsonDocument jsonDoc = QJsonDocument::fromVariant(params);
|
||||
QByteArray payload = jsonDoc.toJson(QJsonDocument::Compact);
|
||||
|
||||
qDebug() << "Calling GetCredentialsForIdentity:" << request.url();
|
||||
qDebug() << "Headers:";
|
||||
foreach (const QByteArray &headerName, request.rawHeaderList()) {
|
||||
qDebug() << headerName << ":" << request.rawHeader(headerName);
|
||||
}
|
||||
qDebug() << "Payload:" << qUtf8Printable(payload);
|
||||
// qDebug() << "Calling GetCredentialsForIdentity:" << request.url();
|
||||
// qDebug() << "Headers:";
|
||||
// foreach (const QByteArray &headerName, request.rawHeaderList()) {
|
||||
// qDebug() << headerName << ":" << request.rawHeader(headerName);
|
||||
// }
|
||||
// qDebug() << "Payload:" << qUtf8Printable(payload);
|
||||
|
||||
QNetworkReply *reply = m_nam->post(request, payload);
|
||||
connect(reply, &QNetworkReply::finished, this, [this, reply]() {
|
||||
reply->deleteLater();
|
||||
if (reply->error() != QNetworkReply::NoError) {
|
||||
qWarning() << "Error calling GetCredentialsForIdentity" << reply->errorString();
|
||||
emit loginResult(LoginErrorUnknownError);
|
||||
return;
|
||||
}
|
||||
QByteArray data = reply->readAll();
|
||||
@ -314,6 +666,7 @@ void AWSClient::getCredentialsForIdentity(const QString &identityId)
|
||||
QJsonDocument jsonDoc = QJsonDocument::fromJson(data, &error);
|
||||
if (error.error != QJsonParseError::NoError) {
|
||||
qWarning() << "Error parsing JSON reply from GetCredentialsForIdentity" << error.errorString();
|
||||
emit loginResult(LoginErrorUnknownError);
|
||||
return;
|
||||
}
|
||||
QVariantMap credentialsMap = jsonDoc.toVariant().toMap().value("Credentials").toMap();
|
||||
@ -330,7 +683,8 @@ void AWSClient::getCredentialsForIdentity(const QString &identityId)
|
||||
settings.setValue("sessionToken", m_sessionToken);
|
||||
settings.setValue("sessionTokenExpiry", m_sessionTokenExpiry);
|
||||
|
||||
qDebug() << "AWS Credentials for Identity received.";
|
||||
qDebug() << "AWS Credentials for Identity received.";// << data;
|
||||
emit loginResult(LoginErrorNoError);
|
||||
|
||||
while (!m_callQueue.isEmpty()) {
|
||||
QueuedCall qc = m_callQueue.takeFirst();
|
||||
@ -360,7 +714,6 @@ bool AWSClient::postToMQTT(const QString &boxId, std::function<void(bool)> callb
|
||||
m_callQueue.append(QueuedCall("postToMQTT", boxId, callback));
|
||||
return true; // So far it looks we're doing ok... let's return true
|
||||
}
|
||||
QString host = "a2addxakg5juii.iot.eu-west-1.amazonaws.com";
|
||||
QString topic = QString("%1/%2/proxy").arg(boxId).arg(QString(m_identityId));
|
||||
|
||||
// This is somehow broken in AWS...
|
||||
@ -378,14 +731,14 @@ bool AWSClient::postToMQTT(const QString &boxId, std::function<void(bool)> callb
|
||||
QByteArray payload = QJsonDocument::fromVariant(params).toJson(QJsonDocument::Compact);
|
||||
|
||||
|
||||
QNetworkRequest request("https://" + host + path);
|
||||
QNetworkRequest request("https://" + m_configs.at(m_usedConfigIndex).mqttEndpoint + path);
|
||||
request.setRawHeader("content-type", "application/json");
|
||||
request.setRawHeader("host", host.toUtf8());
|
||||
request.setRawHeader("host", m_configs.at(m_usedConfigIndex).mqttEndpoint.toUtf8());
|
||||
|
||||
SigV4Utils::signRequest(QNetworkAccessManager::PostOperation, request, region, service, m_accessKeyId, m_secretKey, m_sessionToken, payload);
|
||||
SigV4Utils::signRequest(QNetworkAccessManager::PostOperation, request, m_configs.at(m_usedConfigIndex).region, "iotdata", m_accessKeyId, m_secretKey, m_sessionToken, payload);
|
||||
|
||||
// Workaround MQTT broker url weirdness as described above
|
||||
request.setUrl("https://" + host + path1);
|
||||
request.setUrl("https://" + m_configs.at(m_usedConfigIndex).mqttEndpoint + path1);
|
||||
|
||||
qDebug() << "Posting to MQTT:" << request.url().toString();
|
||||
qDebug() << "HEADERS:";
|
||||
@ -430,7 +783,7 @@ void AWSClient::fetchDevices()
|
||||
return;
|
||||
}
|
||||
qDebug() << "Fetching cloud devices";
|
||||
QUrl url("https://z6368zhf2m.execute-api.eu-west-1.amazonaws.com/dev/devices");
|
||||
QUrl url(QString("https://%1/users/devices").arg(m_configs.at(m_usedConfigIndex).apiEndpoint));
|
||||
QNetworkRequest request(url);
|
||||
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
|
||||
request.setRawHeader("x-api-idToken", m_idToken);
|
||||
@ -483,7 +836,8 @@ void AWSClient::refreshAccessToken()
|
||||
|
||||
|
||||
// Non-working block... Enable this if Amazon ever fixes their API...
|
||||
QUrl url("https://cognito-idp.eu-west-1.amazonaws.com/");
|
||||
QString host = QString("cognito-idp.%1.amazonaws.com").arg(m_configs.at(m_usedConfigIndex).region);
|
||||
QUrl url(QString("https://%1/").arg(host));
|
||||
|
||||
QUrlQuery query;
|
||||
query.addQueryItem("Action", "InitiateAuth");
|
||||
@ -492,12 +846,12 @@ void AWSClient::refreshAccessToken()
|
||||
|
||||
QNetworkRequest request(url);
|
||||
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-amz-json-1.0");
|
||||
request.setRawHeader("Host", "cognito-idp.eu-west-1.amazonaws.com");
|
||||
request.setRawHeader("Host", host.toUtf8());
|
||||
request.setRawHeader("X-Amz-Target", "AWSCognitoIdentityProviderService.InitiateAuth");
|
||||
|
||||
QVariantMap params;
|
||||
params.insert("AuthFlow", "REFRESH_TOKEN_AUTH");
|
||||
params.insert("ClientId", clientId);
|
||||
params.insert("ClientId", m_configs.at(m_usedConfigIndex).clientId);
|
||||
|
||||
QVariantMap authParams;
|
||||
authParams.insert("REFRESH_TOKEN", m_refreshToken);
|
||||
@ -581,7 +935,7 @@ QHash<int, QByteArray> AWSDevices::roleNames() const
|
||||
AWSDevice *AWSDevices::getDevice(const QString &uuid) const
|
||||
{
|
||||
for (int i = 0; i < m_list.count(); i++) {
|
||||
if (m_list.at(i)->id() == uuid) {
|
||||
if (QUuid(m_list.at(i)->id()) == QUuid(uuid)) {
|
||||
return m_list.at(i);
|
||||
}
|
||||
}
|
||||
@ -605,6 +959,25 @@ void AWSDevices::insert(AWSDevice *device)
|
||||
emit countChanged();
|
||||
}
|
||||
|
||||
void AWSDevices::remove(const QString &uuid)
|
||||
{
|
||||
int idx = -1;
|
||||
for (int i = 0; i < m_list.count(); i++) {
|
||||
if (m_list.at(i)->id() == uuid) {
|
||||
idx = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (idx == -1) {
|
||||
qWarning() << "Cannot remove AWS with id" << uuid << "as there is no such device";
|
||||
return;
|
||||
}
|
||||
beginRemoveRows(QModelIndex(), idx, idx);
|
||||
m_list.takeAt(idx)->deleteLater();
|
||||
endRemoveRows();
|
||||
emit countChanged();
|
||||
}
|
||||
|
||||
AWSDevice::AWSDevice(const QString &id, const QString &name, bool online, QObject *parent):
|
||||
QObject (parent),
|
||||
m_id(id),
|
||||
|
||||
@ -46,31 +46,66 @@ public:
|
||||
Q_INVOKABLE AWSDevice* getDevice(const QString &uuid) const;
|
||||
Q_INVOKABLE AWSDevice* get(int index) const;
|
||||
void insert(AWSDevice *device);
|
||||
void remove(const QString &uuid);
|
||||
signals:
|
||||
void countChanged();
|
||||
private:
|
||||
QList<AWSDevice*> m_list;
|
||||
};
|
||||
|
||||
class AWSConfiguration {
|
||||
public:
|
||||
QByteArray clientId;
|
||||
QString poolId;
|
||||
QString identityPoolId;
|
||||
QString certificateEndpoint;
|
||||
QString certificateApiKey;
|
||||
QString certificateVendorId;
|
||||
QString mqttEndpoint;
|
||||
QString region;
|
||||
QString apiEndpoint;
|
||||
};
|
||||
|
||||
class AWSClient : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
Q_PROPERTY(bool isLoggedIn READ isLoggedIn NOTIFY isLoggedInChanged)
|
||||
Q_PROPERTY(QString username READ username NOTIFY isLoggedInChanged)
|
||||
Q_PROPERTY(bool confirmationPending READ confirmationPending NOTIFY confirmationPendingChanged)
|
||||
Q_PROPERTY(QByteArray userId READ userId NOTIFY isLoggedInChanged)
|
||||
Q_PROPERTY(QByteArray idToken READ idToken NOTIFY isLoggedInChanged)
|
||||
Q_PROPERTY(AWSDevices* awsDevices READ awsDevices CONSTANT)
|
||||
|
||||
Q_PROPERTY(int config READ config WRITE setConfig NOTIFY configChanged)
|
||||
|
||||
public:
|
||||
enum LoginError {
|
||||
LoginErrorNoError,
|
||||
LoginErrorInvalidUserOrPass,
|
||||
LoginErrorInvalidCode,
|
||||
LoginErrorUserExists,
|
||||
LoginErrorLimitExceeded,
|
||||
LoginErrorUnknownError
|
||||
};
|
||||
Q_ENUM(LoginError)
|
||||
|
||||
explicit AWSClient(QObject *parent = nullptr);
|
||||
|
||||
bool isLoggedIn() const;
|
||||
QString username() const;
|
||||
QByteArray userId() const;
|
||||
AWSDevices* awsDevices() const;
|
||||
bool confirmationPending() const;
|
||||
|
||||
Q_INVOKABLE void login(const QString &username, const QString &password);
|
||||
Q_INVOKABLE void logout();
|
||||
Q_INVOKABLE void signup(const QString &username, const QString &password);
|
||||
Q_INVOKABLE void confirmRegistration(const QString &code);
|
||||
Q_INVOKABLE void forgotPassword(const QString &username);
|
||||
Q_INVOKABLE void confirmForgotPassword(const QString &username, const QString &code, const QString &newPassword);
|
||||
Q_INVOKABLE void deleteAccount();
|
||||
|
||||
Q_INVOKABLE void unpairDevice(const QString &boxId);
|
||||
|
||||
Q_INVOKABLE void fetchDevices();
|
||||
|
||||
@ -83,10 +118,22 @@ public:
|
||||
|
||||
void fetchCertificate(const QString &uuid, std::function<void(const QByteArray &rootCA, const QByteArray &certificate, const QByteArray &publicKey, const QByteArray &privateKey, const QString &endpoint)> callback);
|
||||
|
||||
int config() const;
|
||||
void setConfig(int index);
|
||||
|
||||
signals:
|
||||
void loginResult(LoginError error);
|
||||
void signupResult(LoginError error);
|
||||
void confirmationResult(LoginError error);
|
||||
void forgotPasswordResult(LoginError error);
|
||||
void confirmForgotPasswordResult(LoginError error);
|
||||
|
||||
void isLoggedInChanged();
|
||||
void confirmationPendingChanged();
|
||||
void devicesFetched();
|
||||
|
||||
void configChanged();
|
||||
|
||||
private:
|
||||
void refreshAccessToken();
|
||||
void getCredentialsForIdentity(const QString &identityId);
|
||||
@ -96,9 +143,12 @@ private:
|
||||
private:
|
||||
QNetworkAccessManager *m_nam = nullptr;
|
||||
|
||||
QString m_userId;
|
||||
QString m_username;
|
||||
QString m_password;
|
||||
|
||||
bool m_confirmationPending = false;
|
||||
|
||||
QByteArray m_accessToken;
|
||||
QDateTime m_accessTokenExpiry;
|
||||
QByteArray m_idToken;
|
||||
@ -114,6 +164,7 @@ private:
|
||||
class QueuedCall {
|
||||
public:
|
||||
QueuedCall(const QString &method): method(method) { }
|
||||
QueuedCall(const QString &method, const QString &boxId): method(method), boxId(boxId) { }
|
||||
QueuedCall(const QString &method, const QString &boxId, std::function<void(bool)> callback): method(method), boxId(boxId), callback(callback) {}
|
||||
QString method;
|
||||
QString boxId;
|
||||
@ -122,6 +173,8 @@ private:
|
||||
|
||||
QList<QueuedCall> m_callQueue;
|
||||
|
||||
QList<AWSConfiguration> m_configs;
|
||||
int m_usedConfigIndex = 0;
|
||||
AWSDevices *m_devices;
|
||||
};
|
||||
|
||||
|
||||
@ -34,7 +34,7 @@ CloudTransport::CloudTransport(AWSClient *awsClient, QObject *parent):
|
||||
QObject::connect(m_remoteproxyConnection, &RemoteProxyConnection::dataReady, this, [this](const QByteArray &data) {
|
||||
emit dataReady(data);
|
||||
});
|
||||
QObject::connect(m_remoteproxyConnection, &RemoteProxyConnection::errorOccured, this, [this] (RemoteProxyConnection::Error error) {
|
||||
QObject::connect(m_remoteproxyConnection, &RemoteProxyConnection::errorOccured, this, [] (QAbstractSocket::SocketError error) {
|
||||
qDebug() << "Remote proxy Error:" << error;
|
||||
// emit NymeaTransportInterface::error(QAbstractSocket::ConnectionRefusedError);
|
||||
});
|
||||
@ -57,7 +57,8 @@ bool CloudTransport::connect(const QUrl &url)
|
||||
|
||||
bool postResult = m_awsClient->postToMQTT(url.host(), [this](bool success) {
|
||||
if (success) {
|
||||
m_remoteproxyConnection->connectServer(QUrl("wss://dev-remoteproxy.nymea.io"));
|
||||
|
||||
m_remoteproxyConnection->connectServer(QUrl("wss://remoteproxy.nymea.io"));
|
||||
// m_remoteproxyConnection->connectServer(QUrl("wss://127.0.0.1:1212"));
|
||||
}
|
||||
});
|
||||
@ -87,9 +88,10 @@ NymeaTransportInterface::ConnectionState CloudTransport::connectionState() const
|
||||
case RemoteProxyConnection::StateConnected:
|
||||
case RemoteProxyConnection::StateAuthenticating:
|
||||
case RemoteProxyConnection::StateReady:
|
||||
case RemoteProxyConnection::StateWaitTunnel:
|
||||
case RemoteProxyConnection::StateAuthenticated:
|
||||
return NymeaTransportInterface::ConnectionStateConnecting;
|
||||
case RemoteProxyConnection::StateDisconnected:
|
||||
case RemoteProxyConnection::StateDiconnecting:
|
||||
return NymeaTransportInterface::ConnectionStateDisconnected;
|
||||
}
|
||||
return ConnectionStateDisconnected;
|
||||
|
||||
@ -92,6 +92,8 @@ QVariant Connections::data(const QModelIndex &index, int role) const
|
||||
return m_connections.at(index.row())->bearerType();
|
||||
case RoleSecure:
|
||||
return m_connections.at(index.row())->secure();
|
||||
case RoleOnline:
|
||||
return m_connections.at(index.row())->online();
|
||||
}
|
||||
return QVariant();
|
||||
}
|
||||
@ -111,10 +113,42 @@ void Connections::addConnection(Connection *connection)
|
||||
connection->setParent(this);
|
||||
beginInsertRows(QModelIndex(), m_connections.count(), m_connections.count());
|
||||
m_connections.append(connection);
|
||||
connect(connection, &Connection::onlineChanged, this, [this, connection]() {
|
||||
int idx = m_connections.indexOf(connection);
|
||||
if (idx < 0) {
|
||||
return;
|
||||
}
|
||||
emit dataChanged(index(idx), index(idx), {RoleOnline});
|
||||
});
|
||||
endInsertRows();
|
||||
emit countChanged();
|
||||
}
|
||||
|
||||
void Connections::removeConnection(Connection *connection)
|
||||
{
|
||||
int idx = m_connections.indexOf(connection);
|
||||
if (idx == -1) {
|
||||
qWarning() << "Cannot remove connections as it's not in this model";
|
||||
return;
|
||||
}
|
||||
beginRemoveRows(QModelIndex(), idx, idx);
|
||||
m_connections.takeAt(idx)->deleteLater();
|
||||
endRemoveRows();
|
||||
emit countChanged();
|
||||
}
|
||||
|
||||
void Connections::removeConnection(int index)
|
||||
{
|
||||
if (index < 0 || index >= m_connections.count()) {
|
||||
qWarning() << "Index out of range. Not removing any connection";
|
||||
return;
|
||||
}
|
||||
beginRemoveRows(QModelIndex(), index, index);
|
||||
m_connections.takeAt(index)->deleteLater();
|
||||
endRemoveRows();
|
||||
emit countChanged();
|
||||
}
|
||||
|
||||
Connection* Connections::get(int index) const
|
||||
{
|
||||
if (index >= 0 && index < m_connections.count()) {
|
||||
@ -130,6 +164,7 @@ QHash<int, QByteArray> Connections::roleNames() const
|
||||
roles.insert(RoleBearerType, "bearerType");
|
||||
roles.insert(RoleName, "name");
|
||||
roles.insert(RoleSecure, "secure");
|
||||
roles.insert(RoleOnline, "online");
|
||||
return roles;
|
||||
}
|
||||
|
||||
|
||||
@ -75,7 +75,8 @@ public:
|
||||
RoleUrl,
|
||||
RoleName,
|
||||
RoleBearerType,
|
||||
RoleSecure
|
||||
RoleSecure,
|
||||
RoleOnline
|
||||
};
|
||||
Q_ENUM(Roles)
|
||||
Connections(QObject* parent = nullptr);
|
||||
@ -83,6 +84,8 @@ public:
|
||||
QVariant data(const QModelIndex & index, int role = Qt::DisplayRole) const override;
|
||||
|
||||
void addConnection(Connection *connection);
|
||||
void removeConnection(Connection *connection);
|
||||
void removeConnection(int index);
|
||||
|
||||
Q_INVOKABLE Connection* find(const QUrl &url) const;
|
||||
Q_INVOKABLE Connection* get(int index) const;
|
||||
|
||||
@ -64,8 +64,24 @@ void DiscoveryModel::addDevice(DiscoveryDevice *device)
|
||||
emit countChanged();
|
||||
}
|
||||
|
||||
void DiscoveryModel::removeDevice(DiscoveryDevice *device)
|
||||
{
|
||||
int idx = m_devices.indexOf(device);
|
||||
if (idx == -1) {
|
||||
qWarning() << "Cannot remove DiscoveryDevice" << device << "as its nit in the model";
|
||||
return;
|
||||
}
|
||||
beginRemoveRows(QModelIndex(), idx, idx);
|
||||
m_devices.takeAt(idx);
|
||||
endRemoveRows();
|
||||
emit countChanged();
|
||||
}
|
||||
|
||||
DiscoveryDevice *DiscoveryModel::get(int index) const
|
||||
{
|
||||
if (index < 0 || index >= m_devices.count()) {
|
||||
return nullptr;
|
||||
}
|
||||
return m_devices.at(index);
|
||||
}
|
||||
|
||||
|
||||
@ -46,6 +46,7 @@ public:
|
||||
QVariant data(const QModelIndex & index, int role = Qt::DisplayRole) const;
|
||||
|
||||
void addDevice(DiscoveryDevice *device);
|
||||
void removeDevice(DiscoveryDevice *device);
|
||||
|
||||
Q_INVOKABLE DiscoveryDevice *get(int index) const;
|
||||
Q_INVOKABLE DiscoveryDevice *find(const QUuid &uuid);
|
||||
|
||||
@ -20,7 +20,13 @@ NymeaDiscovery::NymeaDiscovery(QObject *parent) : QObject(parent)
|
||||
m_bluetooth = new BluetoothServiceDiscovery(m_discoveryModel, this);
|
||||
#endif
|
||||
|
||||
connect(Engine::instance()->awsClient()->awsDevices(), &AWSDevices::countChanged, this, &NymeaDiscovery::syncCloudDevices);
|
||||
m_cloudPollTimer.setInterval(5000);
|
||||
connect(&m_cloudPollTimer, &QTimer::timeout, this, [](){
|
||||
if (Engine::instance()->awsClient()->isLoggedIn()) {
|
||||
Engine::instance()->awsClient()->fetchDevices();
|
||||
}
|
||||
});
|
||||
connect(Engine::instance()->awsClient(), &AWSClient::devicesFetched, this, &NymeaDiscovery::syncCloudDevices);
|
||||
}
|
||||
|
||||
bool NymeaDiscovery::discovering() const
|
||||
@ -44,11 +50,13 @@ void NymeaDiscovery::setDiscovering(bool discovering)
|
||||
syncCloudDevices();
|
||||
Engine::instance()->awsClient()->fetchDevices();
|
||||
}
|
||||
m_cloudPollTimer.start();
|
||||
} else {
|
||||
m_upnp->stopDiscovery();
|
||||
if (m_bluetooth) {
|
||||
m_bluetooth->stopDiscovery();
|
||||
}
|
||||
m_cloudPollTimer.stop();
|
||||
}
|
||||
|
||||
emit discoveringChanged();
|
||||
@ -74,11 +82,31 @@ void NymeaDiscovery::syncCloudDevices()
|
||||
QUrl url;
|
||||
url.setScheme("cloud");
|
||||
url.setHost(d->id());
|
||||
if (!device->connections()->find(url)) {
|
||||
Connection *conn = new Connection(url, Connection::BearerTypeCloud, true, d->id());
|
||||
conn->setOnline(d->online());
|
||||
Connection *conn = device->connections()->find(url);
|
||||
if (!conn) {
|
||||
conn = new Connection(url, Connection::BearerTypeCloud, true, d->id());
|
||||
device->connections()->addConnection(conn);
|
||||
}
|
||||
conn->setOnline(d->online());
|
||||
}
|
||||
|
||||
QList<DiscoveryDevice*> devicesToRemove;
|
||||
for (int i = 0; i < m_discoveryModel->rowCount(); i++) {
|
||||
DiscoveryDevice *device = m_discoveryModel->get(i);
|
||||
for (int j = 0; j < device->connections()->rowCount(); j++) {
|
||||
if (device->connections()->get(j)->bearerType() == Connection::BearerTypeCloud) {
|
||||
if (Engine::instance()->awsClient()->awsDevices()->getDevice(device->uuid().toString()) == nullptr) {
|
||||
device->connections()->removeConnection(j);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (device->connections()->rowCount() == 0) {
|
||||
devicesToRemove.append(device);
|
||||
}
|
||||
}
|
||||
while (!devicesToRemove.isEmpty()) {
|
||||
m_discoveryModel->removeDevice(devicesToRemove.takeFirst());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
#define NYMEADISCOVERY_H
|
||||
|
||||
#include <QObject>
|
||||
#include <QTimer>
|
||||
|
||||
#include "connection/awsclient.h"
|
||||
|
||||
@ -10,6 +11,7 @@ class UpnpDiscovery;
|
||||
class ZeroconfDiscovery;
|
||||
class BluetoothServiceDiscovery;
|
||||
|
||||
|
||||
class NymeaDiscovery : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
@ -38,6 +40,8 @@ private:
|
||||
ZeroconfDiscovery *m_zeroConf = nullptr;
|
||||
BluetoothServiceDiscovery *m_bluetooth = nullptr;
|
||||
|
||||
QTimer m_cloudPollTimer;
|
||||
|
||||
};
|
||||
|
||||
#endif // NYMEADISCOVERY_H
|
||||
|
||||
@ -255,6 +255,7 @@ void UpnpDiscovery::networkReplyFinished(QNetworkReply *reply)
|
||||
bool sslEnabled = url.scheme() == "nymeas" || url.scheme() == "wss";
|
||||
QString displayName = QString("%1:%2").arg(url.host()).arg(url.port());
|
||||
Connection *conn = new Connection(url, Connection::BearerTypeWifi, sslEnabled, displayName);
|
||||
conn->setOnline(true);
|
||||
device->connections()->addConnection(conn);
|
||||
}
|
||||
}
|
||||
|
||||
@ -12,12 +12,14 @@ ZeroconfDiscovery::ZeroconfDiscovery(DiscoveryModel *discoveryModel, QObject *pa
|
||||
m_zeroconfJsonRPC = new QZeroConf(this);
|
||||
connect(m_zeroconfJsonRPC, &QZeroConf::serviceAdded, this, &ZeroconfDiscovery::serviceEntryAdded);
|
||||
connect(m_zeroconfJsonRPC, &QZeroConf::serviceUpdated, this, &ZeroconfDiscovery::serviceEntryAdded);
|
||||
connect(m_zeroconfJsonRPC, &QZeroConf::serviceRemoved, this, &ZeroconfDiscovery::serviceEntryRemoved);
|
||||
m_zeroconfJsonRPC->startBrowser("_jsonrpc._tcp", QAbstractSocket::AnyIPProtocol);
|
||||
qDebug() << "ZeroConf: Created service browser for _jsonrpc._tcp:" << m_zeroconfJsonRPC->browserExists();
|
||||
|
||||
m_zeroconfWebSocket = new QZeroConf(this);
|
||||
connect(m_zeroconfWebSocket, &QZeroConf::serviceAdded, this, &ZeroconfDiscovery::serviceEntryAdded);
|
||||
connect(m_zeroconfWebSocket, &QZeroConf::serviceUpdated, this, &ZeroconfDiscovery::serviceEntryAdded);
|
||||
connect(m_zeroconfWebSocket, &QZeroConf::serviceRemoved, this, &ZeroconfDiscovery::serviceEntryRemoved);
|
||||
m_zeroconfWebSocket->startBrowser("_ws._tcp", QAbstractSocket::AnyIPProtocol);
|
||||
qDebug() << "TeroConf: Created service browser for _ws._tcp:" << m_zeroconfWebSocket->browserExists();
|
||||
#else
|
||||
@ -84,14 +86,72 @@ void ZeroconfDiscovery::serviceEntryAdded(const QZeroConfService &entry)
|
||||
} else {
|
||||
url.setScheme(sslEnabled ? "wss" : "ws");
|
||||
}
|
||||
// entry
|
||||
url.setHost(!entry.ip().isNull() ? entry.ip().toString() : entry.ipv6().toString());
|
||||
url.setPort(entry.port());
|
||||
if (!device->connections()->find(url)){
|
||||
qDebug() << "Zeroconf: Adding new connection to host:" << device->name() << url.toString();
|
||||
QString displayName = QString("%1:%2").arg(url.host()).arg(url.port());
|
||||
Connection *connection = new Connection(url, Connection::BearerTypeWifi, sslEnabled, displayName);
|
||||
connection->setOnline(true);
|
||||
device->connections()->addConnection(connection);
|
||||
}
|
||||
}
|
||||
|
||||
void ZeroconfDiscovery::serviceEntryRemoved(const QZeroConfService &entry)
|
||||
{
|
||||
if (!entry.name().startsWith("nymea") || (entry.ip().isNull() && entry.ipv6().isNull())) {
|
||||
return;
|
||||
}
|
||||
|
||||
QString uuid;
|
||||
bool sslEnabled = false;
|
||||
QString serverName;
|
||||
QString version;
|
||||
foreach (const QByteArray &key, entry.txt().keys()) {
|
||||
QPair<QString, QString> txtRecord = qMakePair<QString, QString>(key, entry.txt().value(key));
|
||||
if (!sslEnabled && txtRecord.first == "sslEnabled") {
|
||||
sslEnabled = (txtRecord.second == "true");
|
||||
}
|
||||
if (txtRecord.first == "uuid") {
|
||||
uuid = txtRecord.second;
|
||||
}
|
||||
if (txtRecord.first == "name") {
|
||||
serverName = txtRecord.second;
|
||||
}
|
||||
if (txtRecord.first == "serverVersion") {
|
||||
version = txtRecord.second;
|
||||
}
|
||||
}
|
||||
|
||||
// qDebug() << "Zeroconf: Service entry removed" << entry.name();
|
||||
|
||||
DiscoveryDevice* device = m_discoveryModel->find(uuid);
|
||||
if (!device) {
|
||||
// Nothing to do...
|
||||
return;
|
||||
}
|
||||
|
||||
QUrl url;
|
||||
if (entry.type() == "_jsonrpc._tcp") {
|
||||
url.setScheme(sslEnabled ? "nymeas" : "nymea");
|
||||
} else {
|
||||
url.setScheme(sslEnabled ? "wss" : "ws");
|
||||
}
|
||||
url.setHost(!entry.ip().isNull() ? entry.ip().toString() : entry.ipv6().toString());
|
||||
url.setPort(entry.port());
|
||||
Connection *connection = device->connections()->find(url);
|
||||
if (!connection){
|
||||
// Connection url not found...
|
||||
return;
|
||||
}
|
||||
|
||||
// Ok, now we need to remove it
|
||||
device->connections()->removeConnection(connection);
|
||||
|
||||
// And if there aren't any connections left, remove the entire device
|
||||
if (device->connections()->rowCount() == 0) {
|
||||
qDebug() << "Zeroconf: Removing connection from host:" << device->name() << url.toString();
|
||||
m_discoveryModel->removeDevice(device);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
@ -28,6 +28,7 @@ private:
|
||||
|
||||
private slots:
|
||||
void serviceEntryAdded(const QZeroConfService &entry);
|
||||
void serviceEntryRemoved(const QZeroConfService &entry);
|
||||
#endif
|
||||
};
|
||||
|
||||
|
||||
@ -236,6 +236,7 @@ int JsonRpcClient::requestPushButtonAuth(const QString &deviceName)
|
||||
|
||||
void JsonRpcClient::setupRemoteAccess(const QString &idToken, const QString &userId)
|
||||
{
|
||||
qDebug() << "Calling SetupRemoteAccess";
|
||||
QVariantMap params;
|
||||
params.insert("idToken", idToken);
|
||||
params.insert("userId", userId);
|
||||
|
||||
@ -92,3 +92,6 @@ BR=$$BRANDING
|
||||
|
||||
target.path = /usr/bin
|
||||
INSTALLS += target
|
||||
|
||||
DISTFILES += \
|
||||
ui/components/BusyOverlay.qml
|
||||
|
||||
@ -155,12 +155,10 @@
|
||||
<file>ui/images/network-vpn.svg</file>
|
||||
<file>translations/nymea-app-de_DE.qm</file>
|
||||
<file>translations/nymea-app-en_US.qm</file>
|
||||
<file>ui/AppSettingsPage.qml</file>
|
||||
<file>ui/images/stock_application.svg</file>
|
||||
<file>ui/delegates/ThingDelegate.qml</file>
|
||||
<file>ui/images/network-secure.svg</file>
|
||||
<file>ui/images/lock-broken.svg</file>
|
||||
<file>ui/AboutPage.qml</file>
|
||||
<file>ui/images/sort-listitem.svg</file>
|
||||
<file>ui/devicepages/ShutterDevicePage.qml</file>
|
||||
<file>ui/images/shutter/shutter-000.svg</file>
|
||||
@ -242,8 +240,13 @@
|
||||
<file>ui/KeyboardLoader.qml</file>
|
||||
<file>ui/images/cloud.svg</file>
|
||||
<file>ui/system/CloudSettingsPage.qml</file>
|
||||
<file>ui/connection/CloudLoginPage.qml</file>
|
||||
<file>ui/images/cloud-offline.svg</file>
|
||||
<file>ui/images/cloud-error.svg</file>
|
||||
<file>ui/components/BusyOverlay.qml</file>
|
||||
<file>ui/components/AWSPasswordTextField.qml</file>
|
||||
<file>ui/appsettings/AboutPage.qml</file>
|
||||
<file>ui/appsettings/AppSettingsPage.qml</file>
|
||||
<file>ui/appsettings/DeveloperOptionsPage.qml</file>
|
||||
<file>ui/appsettings/CloudLoginPage.qml</file>
|
||||
</qresource>
|
||||
</RCC>
|
||||
|
||||
@ -17,7 +17,7 @@ Page {
|
||||
model: ListModel {
|
||||
ListElement { iconSource: "../images/share.svg"; text: qsTr("Configure things"); page: "EditDevicesPage.qml" }
|
||||
ListElement { iconSource: "../images/magic.svg"; text: qsTr("Magic"); page: "MagicPage.qml" }
|
||||
ListElement { iconSource: "../images/stock_application.svg"; text: qsTr("App settings"); page: "AppSettingsPage.qml" }
|
||||
ListElement { iconSource: "../images/stock_application.svg"; text: qsTr("App settings"); page: "appsettings/AppSettingsPage.qml" }
|
||||
ListElement { iconSource: "../images/settings.svg"; text: qsTr("System settings"); page: "SettingsPage.qml" }
|
||||
}
|
||||
|
||||
|
||||
@ -39,6 +39,14 @@ ApplicationWindow {
|
||||
property string graphStyle: "bars"
|
||||
property string style: "light"
|
||||
property int currentMainViewIndex: 0
|
||||
property bool showHiddenOptions: false
|
||||
property int cloudEnvironment: 0
|
||||
}
|
||||
|
||||
Binding {
|
||||
target: Engine.awsClient
|
||||
property: "config"
|
||||
value: settings.cloudEnvironment
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
|
||||
@ -3,7 +3,7 @@ import QtQuick.Controls 2.1
|
||||
import QtQuick.Controls.Material 2.1
|
||||
import QtQuick.Layouts 1.1
|
||||
import Nymea 1.0
|
||||
import "components"
|
||||
import "../components"
|
||||
|
||||
Page {
|
||||
id: root
|
||||
@ -27,10 +27,29 @@ Page {
|
||||
spacing: app.margins
|
||||
|
||||
Image {
|
||||
id: logo
|
||||
Layout.preferredHeight: app.iconSize * 2
|
||||
Layout.preferredWidth: height
|
||||
fillMode: Image.PreserveAspectFit
|
||||
source: "qrc:/styles/%1/logo.svg".arg(styleController.currentStyle)
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
property int clickCounter: 0
|
||||
onClicked: {
|
||||
clickCounter++;
|
||||
if (clickCounter >= 10) {
|
||||
settings.showHiddenOptions = !settings.showHiddenOptions
|
||||
var dialog = Qt.createComponent(Qt.resolvedUrl("../components/MeaDialog.qml"));
|
||||
var text = settings.showHiddenOptions
|
||||
? qsTr("Developer options are now enabled. If you have found this by accident, it is most likely not of any use for you. It will just enable some nerdy developer gibberish in the app. Tap the icon another 10 times to disable it again.")
|
||||
: qsTr("Developer options are now disabled.")
|
||||
var popup = dialog.createObject(app, {headerIcon: "../images/dialog-warning-symbolic.svg", title: qsTr("Howdy cowboy!"), text: text})
|
||||
popup.open();
|
||||
clickCounter = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
GridLayout {
|
||||
@ -83,60 +102,25 @@ Page {
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
|
||||
ItemDelegate {
|
||||
MeaListItemDelegate {
|
||||
Layout.fillWidth: true
|
||||
|
||||
contentItem: RowLayout {
|
||||
Label {
|
||||
Layout.fillWidth: true
|
||||
text: qsTr("Visit the nymea website")
|
||||
}
|
||||
Image {
|
||||
source: "images/next.svg"
|
||||
Layout.preferredHeight: parent.height
|
||||
Layout.preferredWidth: height
|
||||
}
|
||||
}
|
||||
|
||||
text: qsTr("Visit the nymea website")
|
||||
onClicked: {
|
||||
Qt.openUrlExternally("https://nymea.io")
|
||||
}
|
||||
}
|
||||
|
||||
ItemDelegate {
|
||||
MeaListItemDelegate {
|
||||
Layout.fillWidth: true
|
||||
|
||||
contentItem: RowLayout {
|
||||
Label {
|
||||
Layout.fillWidth: true
|
||||
text: qsTr("Visit GitHub page")
|
||||
}
|
||||
Image {
|
||||
source: "images/next.svg"
|
||||
Layout.preferredHeight: parent.height
|
||||
Layout.preferredWidth: height
|
||||
}
|
||||
}
|
||||
text: qsTr("Visit GitHub page")
|
||||
onClicked: {
|
||||
Qt.openUrlExternally("https://github.com/guh/nymea-app")
|
||||
}
|
||||
}
|
||||
|
||||
ItemDelegate {
|
||||
MeaListItemDelegate {
|
||||
Layout.fillWidth: true
|
||||
|
||||
contentItem: RowLayout {
|
||||
Label {
|
||||
Layout.fillWidth: true
|
||||
text: qsTr("View license text")
|
||||
}
|
||||
Image {
|
||||
source: "images/next.svg"
|
||||
Layout.preferredHeight: parent.height
|
||||
Layout.preferredWidth: height
|
||||
}
|
||||
}
|
||||
|
||||
text: qsTr("View license text")
|
||||
onClicked: {
|
||||
pageStack.push(licenseTextComponent)
|
||||
}
|
||||
@ -155,7 +139,7 @@ Page {
|
||||
Layout.preferredHeight: app.iconSize * 2
|
||||
Layout.preferredWidth: height
|
||||
fillMode: Image.PreserveAspectFit
|
||||
source: "images/Built_with_Qt_RGB_logo_vertical.svg"
|
||||
source: "qrc:/ui/images/Built_with_Qt_RGB_logo_vertical.svg"
|
||||
sourceSize.width: app.iconSize * 2
|
||||
sourceSize.height: app.iconSize * 2
|
||||
}
|
||||
@ -166,21 +150,9 @@ Page {
|
||||
wrapMode: Text.WordWrap
|
||||
}
|
||||
}
|
||||
ItemDelegate {
|
||||
MeaListItemDelegate {
|
||||
Layout.fillWidth: true
|
||||
|
||||
contentItem: RowLayout {
|
||||
Label {
|
||||
Layout.fillWidth: true
|
||||
text: qsTr("Visit the Qt website")
|
||||
}
|
||||
Image {
|
||||
source: "images/next.svg"
|
||||
Layout.preferredHeight: parent.height
|
||||
Layout.preferredWidth: height
|
||||
}
|
||||
}
|
||||
|
||||
text: qsTr("Visit the Qt website")
|
||||
onClicked: {
|
||||
Qt.openUrlExternally("https://www.qt.io")
|
||||
}
|
||||
@ -3,7 +3,7 @@ import QtQuick.Controls 2.1
|
||||
import QtQuick.Controls.Material 2.1
|
||||
import QtQuick.Layouts 1.1
|
||||
import Nymea 1.0
|
||||
import "components"
|
||||
import "../components"
|
||||
|
||||
Page {
|
||||
id: root
|
||||
@ -114,7 +114,7 @@ Page {
|
||||
Layout.fillWidth: true
|
||||
text: qsTr("Cloud login")
|
||||
iconName: "../images/cloud.svg"
|
||||
onClicked: pageStack.push(Qt.resolvedUrl("connection/CloudLoginPage.qml"))
|
||||
onClicked: pageStack.push(Qt.resolvedUrl("CloudLoginPage.qml"))
|
||||
}
|
||||
MeaListItemDelegate {
|
||||
Layout.fillWidth: true
|
||||
@ -122,6 +122,13 @@ Page {
|
||||
iconName: "../images/info.svg"
|
||||
onClicked: pageStack.push(Qt.resolvedUrl("AboutPage.qml"))
|
||||
}
|
||||
MeaListItemDelegate {
|
||||
Layout.fillWidth: true
|
||||
visible: settings.showHiddenOption
|
||||
text: qsTr("Developer options")
|
||||
iconName: "../images/configure.svg"
|
||||
onClicked: pageStack.push(Qt.resolvedUrl("DeveloperOptionsPage.qml"))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
505
nymea-app/ui/appsettings/CloudLoginPage.qml
Normal file
505
nymea-app/ui/appsettings/CloudLoginPage.qml
Normal file
@ -0,0 +1,505 @@
|
||||
import QtQuick 2.9
|
||||
import QtQuick.Controls 2.2
|
||||
import QtQuick.Layouts 1.3
|
||||
import Nymea 1.0
|
||||
import "../components"
|
||||
|
||||
Page {
|
||||
id: root
|
||||
header: GuhHeader {
|
||||
text: qsTr("Cloud login")
|
||||
onBackPressed: pageStack.pop()
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
if (Engine.awsClient.isLoggedIn) {
|
||||
Engine.awsClient.fetchDevices();
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: Engine.awsClient
|
||||
onLoginResult: {
|
||||
busyOverlay.shown = false;
|
||||
if (error === AWSClient.LoginErrorNoError) {
|
||||
Engine.awsClient.fetchDevices();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
anchors { left: parent.left; top: parent.top; right: parent.right }
|
||||
visible: Engine.awsClient.isLoggedIn
|
||||
Label {
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: app.margins
|
||||
Layout.leftMargin: app.margins
|
||||
Layout.rightMargin: app.margins
|
||||
wrapMode: Text.WordWrap
|
||||
text: qsTr("Logged in as %1").arg(Engine.awsClient.username)
|
||||
}
|
||||
|
||||
Button {
|
||||
Layout.fillWidth: true
|
||||
Layout.margins: app.margins
|
||||
text: qsTr("Log out")
|
||||
onClicked: {
|
||||
logoutDialog.open()
|
||||
}
|
||||
}
|
||||
|
||||
ThinDivider {}
|
||||
|
||||
Label {
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: app.margins
|
||||
Layout.leftMargin: app.margins
|
||||
Layout.rightMargin: app.margins
|
||||
wrapMode: Text.WordWrap
|
||||
text: Engine.awsClient.awsDevices.count === 0 ?
|
||||
qsTr("There are no boxes connected to your cloud yet.") :
|
||||
qsTr("There are %n boxes connected to your cloud", "", Engine.awsClient.awsDevices.count)
|
||||
}
|
||||
ListView {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
model: Engine.awsClient.awsDevices
|
||||
delegate: MeaListItemDelegate {
|
||||
width: parent.width
|
||||
text: model.name
|
||||
subText: model.id
|
||||
progressive: false
|
||||
prominentSubText: false
|
||||
canDelete: true
|
||||
iconName: "../images/cloud.svg"
|
||||
secondaryIconName: model.online ? "../images/cloud.svg" : "../images/cloud-offline.svg"
|
||||
onDeleteClicked: {
|
||||
Engine.awsClient.unpairDevice(model.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MeaDialog {
|
||||
id: logoutDialog
|
||||
title: qsTr("Goodbye")
|
||||
// Deleting user profile not working in cloud yet
|
||||
// text: qsTr("Sorry to see you go. If you log out you won't be able to connect to %1 boxes remotely any more. However, you can come back any time, we'll keep your user account. If you whish to completely delete your account and all the data associated with it, check the box below before hitting ok.").arg(app.systemName)
|
||||
text: qsTr("Sorry to see you go. If you log out you won't be able to connect to %1 boxes remotely any more. However, you can come back any time.").arg(app.systemName)
|
||||
headerIcon: "../images/dialog-warning-symbolic.svg"
|
||||
standardButtons: Dialog.Cancel | Dialog.Ok
|
||||
|
||||
// RowLayout {
|
||||
// CheckBox {
|
||||
// id: deleteCheckbox
|
||||
// }
|
||||
// Label {
|
||||
// Layout.fillWidth: true
|
||||
// wrapMode: Text.WordWrap
|
||||
// text: qsTr("Delete my account")
|
||||
// }
|
||||
// }
|
||||
|
||||
onAccepted: {
|
||||
// if (deleteCheckbox.checked) {
|
||||
// Engine.awsClient.deleteAccount()
|
||||
// } else {
|
||||
Engine.awsClient.logout()
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
anchors { left: parent.left; right: parent.right; top: parent.top }
|
||||
visible: !Engine.awsClient.isLoggedIn
|
||||
Label {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: app.margins; Layout.rightMargin: app.margins; Layout.topMargin: app.margins
|
||||
wrapMode: Text.WordWrap
|
||||
text: qsTr("Log in to %1:cloud in order to connect to %1 boxes from anywhere.").arg(app.systemName)
|
||||
}
|
||||
|
||||
Label {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: app.margins; Layout.rightMargin: app.margins; Layout.topMargin: app.margins
|
||||
text: "Username (e-mail)"
|
||||
}
|
||||
TextField {
|
||||
id: usernameTextField
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: app.margins; Layout.rightMargin: app.margins
|
||||
placeholderText: "john.smith@cooldomain.com"
|
||||
inputMethodHints: Qt.ImhEmailCharactersOnly
|
||||
validator: RegExpValidator { regExp:/\w+([-+.']\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*/ }
|
||||
}
|
||||
Label {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: app.margins; Layout.rightMargin: app.margins; Layout.topMargin: app.margins
|
||||
text: qsTr("Password")
|
||||
}
|
||||
TextField {
|
||||
id: passwordTextField
|
||||
Layout.leftMargin: app.margins; Layout.rightMargin: app.margins
|
||||
Layout.fillWidth: true
|
||||
echoMode: TextInput.Password
|
||||
}
|
||||
|
||||
Button {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: app.margins; Layout.rightMargin: app.margins; Layout.topMargin: app.margins
|
||||
text: qsTr("OK")
|
||||
enabled: usernameTextField.acceptableInput
|
||||
onClicked: {
|
||||
busyOverlay.shown = true
|
||||
Engine.awsClient.login(usernameTextField.text, passwordTextField.text);
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: Engine.awsClient
|
||||
onLoginResult: {
|
||||
errorLabel.visible = (error !== AWSClient.LoginErrorNoError)
|
||||
}
|
||||
}
|
||||
|
||||
Label {
|
||||
id: errorLabel
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: app.margins; Layout.rightMargin: app.margins; Layout.bottomMargin: app.margins
|
||||
wrapMode: Text.WordWrap
|
||||
text: qsTr("Failed to log in. Please try again. Do you perhaps have <a href=\"#\">forgotten your password?</a>")
|
||||
font.pixelSize: app.smallFont
|
||||
color: "red"
|
||||
visible: false
|
||||
onLinkActivated: {
|
||||
pageStack.push(resetPasswordComponent, {email: usernameTextField.text})
|
||||
}
|
||||
}
|
||||
|
||||
ThinDivider {}
|
||||
|
||||
Label {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: app.margins; Layout.rightMargin: app.margins; Layout.topMargin: app.margins
|
||||
wrapMode: Text.WordWrap
|
||||
text: qsTr("Don't have a user yet?")
|
||||
}
|
||||
|
||||
Button {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: app.margins; Layout.rightMargin: app.margins; Layout.topMargin: app.margins
|
||||
text: qsTr("Sign Up")
|
||||
onClicked: {
|
||||
pageStack.push(signupPageComponent)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
BusyOverlay {
|
||||
id: busyOverlay
|
||||
}
|
||||
|
||||
Component {
|
||||
id: signupPageComponent
|
||||
Page {
|
||||
id: signupPage
|
||||
header: GuhHeader {
|
||||
text: qsTr("Sign up")
|
||||
onBackPressed: pageStack.pop()
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
anchors { left: parent.left; top: parent.top; right: parent.right }
|
||||
|
||||
Label {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: app.margins; Layout.rightMargin: app.margins; Layout.topMargin: app.margins
|
||||
wrapMode: Text.WordWrap
|
||||
text: qsTr("Welcome to %1:cloud.").arg(app.systemName)
|
||||
color: app.accentColor
|
||||
font.pixelSize: app.largeFont
|
||||
}
|
||||
|
||||
Label {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: app.margins; Layout.rightMargin: app.margins; Layout.topMargin: app.margins
|
||||
wrapMode: Text.WordWrap
|
||||
text: qsTr("Please enter your email address and pick a password in order to create a new account.")
|
||||
}
|
||||
|
||||
Label {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: app.margins; Layout.rightMargin: app.margins; Layout.topMargin: app.margins
|
||||
text: "Username (e-mail)"
|
||||
}
|
||||
TextField {
|
||||
id: usernameTextField
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: app.margins; Layout.rightMargin: app.margins
|
||||
placeholderText: "john.smith@cooldomain.com"
|
||||
inputMethodHints: Qt.ImhEmailCharactersOnly
|
||||
validator: RegExpValidator { regExp:/\w+([-+.']\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*/ }
|
||||
}
|
||||
Label {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: app.margins; Layout.rightMargin: app.margins; Layout.topMargin: app.margins
|
||||
text: qsTr("Password")
|
||||
}
|
||||
AWSPasswordTextField {
|
||||
id: passwordTextField
|
||||
Layout.leftMargin: app.margins; Layout.rightMargin: app.margins
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
Button {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: app.margins; Layout.rightMargin: app.margins; Layout.topMargin: app.margins
|
||||
text: qsTr("Sign Up")
|
||||
enabled: usernameTextField.acceptableInput && passwordTextField.isValidPassword
|
||||
onClicked: {
|
||||
Engine.awsClient.signup(usernameTextField.text, passwordTextField.password)
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: Engine.awsClient
|
||||
onSignupResult: {
|
||||
switch (error) {
|
||||
case AWSClient.LoginErrorNoError:
|
||||
signUpResultLabel.text = ""
|
||||
pageStack.push(enterCodeComponent)
|
||||
break;
|
||||
case AWSClient.LoginErrorInvalidUserOrPass:
|
||||
signUpResultLabel.text = qsTr("The given username or password are not valid.")
|
||||
break;
|
||||
default:
|
||||
signUpResultLabel.text = qsTr("Uh oh, something went wrong. Please try again.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Label {
|
||||
id: signUpResultLabel
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: app.margins; Layout.rightMargin: app.margins; Layout.topMargin: app.margins
|
||||
wrapMode: Text.WordWrap
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: enterCodeComponent
|
||||
Page {
|
||||
header: GuhHeader {
|
||||
text: qsTr("Confirm account")
|
||||
onBackPressed: pageStack.pop()
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
anchors { left: parent.left; top: parent.top; right: parent.right }
|
||||
|
||||
Label {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: app.margins; Layout.rightMargin: app.margins; Layout.topMargin: app.margins
|
||||
wrapMode: Text.WordWrap
|
||||
text: qsTr("Thanks for signing up. We will send you an email with a confirmation code. Please enter that code in the field below.")
|
||||
}
|
||||
|
||||
TextField {
|
||||
id: confirmationCodeTextField
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: app.margins; Layout.rightMargin: app.margins; Layout.topMargin: app.margins
|
||||
inputMethodHints: Qt.ImhDigitsOnly
|
||||
}
|
||||
|
||||
Button {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: app.margins; Layout.rightMargin: app.margins; Layout.topMargin: app.margins
|
||||
text: qsTr("OK")
|
||||
onClicked: {
|
||||
Engine.awsClient.confirmRegistration(confirmationCodeTextField.text)
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: Engine.awsClient
|
||||
onConfirmationResult: {
|
||||
switch (error) {
|
||||
case AWSClient.LoginErrorNoError:
|
||||
confirmResultLabel.text = ""
|
||||
pageStack.pop(root)
|
||||
break;
|
||||
case AWSClient.LoginErrorUserExists:
|
||||
confirmResultLabel.text = qsTr("The given user already exists. Did you forget the password?")
|
||||
break;
|
||||
case AWSClient.LoginErrorInvalidCode:
|
||||
confirmResultLabel.text = qsTr("That wasn't the right code. Please try again.")
|
||||
break;
|
||||
case AWSClient.LoginErrorUnknownError:
|
||||
confirmResultLabel.text = qsTr("Uh oh, something went wrong. Please try again.")
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Label {
|
||||
id: confirmResultLabel
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: app.margins; Layout.rightMargin: app.margins; Layout.topMargin: app.margins
|
||||
wrapMode: Text.WordWrap
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: resetPasswordComponent
|
||||
Page {
|
||||
id: resetPasswordPage
|
||||
|
||||
property alias email: emailTextField.text
|
||||
|
||||
header: GuhHeader {
|
||||
text: qsTr("Reset password")
|
||||
onBackPressed: pageStack.pop()
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: Engine.awsClient
|
||||
onForgotPasswordResult: {
|
||||
busyOverlay.shown = false
|
||||
if (error !== AWSClient.LoginErrorNoError) {
|
||||
var errorDialog = Qt.createComponent(Qt.resolvedUrl("../components/ErrorDialog.qml"));
|
||||
var text = qsTr("Sorry, this wasn't right. Did you misspell the email address?");
|
||||
if (error === AWSClient.LoginErrorLimitExceeded) {
|
||||
text = qsTr("Sorry, there were too many attempts. Please try again after some time.")
|
||||
}
|
||||
var popup = errorDialog.createObject(app, {text: text})
|
||||
popup.open()
|
||||
return;
|
||||
}
|
||||
pageStack.push(confirmResetPasswordComponent, {email: emailTextField.text })
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
anchors { left: parent.left; top: parent.top; right: parent.right }
|
||||
spacing: app.margins
|
||||
|
||||
Label {
|
||||
Layout.fillWidth: true; Layout.leftMargin: app.margins; Layout.rightMargin: app.margins; Layout.topMargin: app.margins
|
||||
wrapMode: Text.WordWrap
|
||||
text: qsTr("Password forgotten?")
|
||||
font.pixelSize: app.largeFont
|
||||
color: app.accentColor
|
||||
}
|
||||
Label {
|
||||
Layout.fillWidth: true; Layout.leftMargin: app.margins; Layout.rightMargin: app.margins
|
||||
wrapMode: Text.WordWrap
|
||||
text: qsTr("No problem. Enter your email address here and we'll send you a confirmation code to change your password.")
|
||||
}
|
||||
TextField {
|
||||
id: emailTextField
|
||||
Layout.fillWidth: true; Layout.leftMargin: app.margins; Layout.rightMargin: app.margins
|
||||
}
|
||||
Button {
|
||||
Layout.fillWidth: true; Layout.leftMargin: app.margins; Layout.rightMargin: app.margins
|
||||
text: qsTr("Reset password")
|
||||
onClicked: {
|
||||
Engine.awsClient.forgotPassword(emailTextField.text)
|
||||
busyOverlay.shown = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
BusyOverlay {
|
||||
id: busyOverlay
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: confirmResetPasswordComponent
|
||||
|
||||
Page {
|
||||
id: confirmResetPasswordPage
|
||||
|
||||
Connections {
|
||||
target: Engine.awsClient
|
||||
onConfirmForgotPasswordResult: {
|
||||
busyOverlay.shown = false
|
||||
if (error !== AWSClient.LoginErrorNoError) {
|
||||
var errorDialog = Qt.createComponent(Qt.resolvedUrl("../components/ErrorDialog.qml"));
|
||||
var popup = errorDialog.createObject(app, {text: qsTr("Sorry, couldn't reset your password. Did you enter the wrong confirmation code?")})
|
||||
popup.open()
|
||||
return;
|
||||
}
|
||||
var dialog = Qt.createComponent(Qt.resolvedUrl("../components/MeaDialog.qml"));
|
||||
var popup = dialog.createObject(app, {headerIcon: "../images/tick.svg", title: qsTr("Yay!"), text: qsTr("Your password has been reset.")})
|
||||
popup.accepted.connect(function() {
|
||||
pageStack.pop(root);
|
||||
})
|
||||
popup.open()
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
property string email
|
||||
header: GuhHeader {
|
||||
text: qsTr("Reset password")
|
||||
onBackPressed: pageStack.pop()
|
||||
}
|
||||
ColumnLayout {
|
||||
anchors { left: parent.left; top: parent.top; right: parent.right }
|
||||
spacing: app.margins
|
||||
|
||||
Label {
|
||||
Layout.fillWidth: true; Layout.leftMargin: app.margins; Layout.rightMargin: app.margins; Layout.topMargin: app.margins
|
||||
wrapMode: Text.WordWrap
|
||||
text: qsTr("Check your email!")
|
||||
color: app.accentColor
|
||||
font.pixelSize: app.largeFont
|
||||
}
|
||||
|
||||
Label {
|
||||
Layout.fillWidth: true; Layout.leftMargin: app.margins; Layout.rightMargin: app.margins;
|
||||
wrapMode: Text.WordWrap
|
||||
text: qsTr("Enter the confirmation code you've received and a new password for your user %1.").arg(confirmResetPasswordPage.email)
|
||||
}
|
||||
|
||||
Label {
|
||||
Layout.fillWidth: true; Layout.leftMargin: app.margins; Layout.rightMargin: app.margins;
|
||||
text: qsTr("Confirmation code:")
|
||||
}
|
||||
|
||||
TextField {
|
||||
id: codeTextField
|
||||
Layout.fillWidth: true; Layout.leftMargin: app.margins; Layout.rightMargin: app.margins
|
||||
}
|
||||
Label {
|
||||
Layout.fillWidth: true; Layout.leftMargin: app.margins; Layout.rightMargin: app.margins;
|
||||
text: qsTr("Pick a new password:")
|
||||
}
|
||||
|
||||
AWSPasswordTextField {
|
||||
id: passwordTextField
|
||||
Layout.fillWidth: true; Layout.leftMargin: app.margins; Layout.rightMargin: app.margins
|
||||
}
|
||||
|
||||
Button {
|
||||
Layout.fillWidth: true; Layout.leftMargin: app.margins; Layout.rightMargin: app.margins
|
||||
text: qsTr("Reset password")
|
||||
enabled: passwordTextField.isValidPassword && codeTextField.text.length > 0
|
||||
onClicked: {
|
||||
busyOverlay.shown = true
|
||||
Engine.awsClient.confirmForgotPassword(confirmResetPasswordPage.email, codeTextField.text, passwordTextField.password)
|
||||
}
|
||||
}
|
||||
BusyOverlay {
|
||||
id: busyOverlay
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
38
nymea-app/ui/appsettings/DeveloperOptionsPage.qml
Normal file
38
nymea-app/ui/appsettings/DeveloperOptionsPage.qml
Normal file
@ -0,0 +1,38 @@
|
||||
import QtQuick 2.9
|
||||
import QtQuick.Controls 2.2
|
||||
import QtQuick.Layouts 1.3
|
||||
import Nymea 1.0
|
||||
import "../components"
|
||||
|
||||
Page {
|
||||
id: root
|
||||
header: GuhHeader {
|
||||
text: qsTr("Developer options")
|
||||
backButtonVisible: true
|
||||
onBackPressed: pageStack.pop()
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
anchors { left: parent.left; top: parent.top; right: parent.right }
|
||||
|
||||
RowLayout {
|
||||
Layout.leftMargin: app.margins; Layout.rightMargin: app.margins
|
||||
|
||||
Label {
|
||||
Layout.fillWidth: true
|
||||
text: qsTr("Cloud environment")
|
||||
}
|
||||
|
||||
ComboBox {
|
||||
currentIndex: app.settings.cloudEnvironment
|
||||
model: [qsTr("Community"), qsTr("Testing")]
|
||||
onActivated: {
|
||||
app.settings.cloudEnvironment = index;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
55
nymea-app/ui/components/AWSPasswordTextField.qml
Normal file
55
nymea-app/ui/components/AWSPasswordTextField.qml
Normal file
@ -0,0 +1,55 @@
|
||||
import QtQuick 2.9
|
||||
import QtQuick.Controls 2.2
|
||||
import QtQuick.Layouts 1.2
|
||||
|
||||
ColumnLayout {
|
||||
id: root
|
||||
|
||||
readonly property alias password: passwordTextField.text
|
||||
|
||||
readonly property bool isValidPassword: isLongEnough && hasLower && hasUpper && hasNumbers && hasSpecialChar && confirmationMatches
|
||||
|
||||
readonly property bool isLongEnough: passwordTextField.text.length >= 12
|
||||
readonly property bool hasLower: passwordTextField.text.search(/[a-z]/) >= 0
|
||||
readonly property bool hasUpper: passwordTextField.text.search(/[A-Z/]/) >= 0
|
||||
readonly property bool hasNumbers: passwordTextField.text.search(/[0-9]/) >= 0
|
||||
readonly property bool hasSpecialChar: passwordTextField.text.search(/[\*]/) >= 0
|
||||
readonly property bool confirmationMatches: passwordTextField.text === confirmationPasswordTextField.text
|
||||
|
||||
TextField {
|
||||
id: passwordTextField
|
||||
Layout.fillWidth: true
|
||||
echoMode: TextInput.Password
|
||||
placeholderText: qsTr("Pick a password")
|
||||
}
|
||||
|
||||
Label {
|
||||
Layout.fillWidth: true
|
||||
wrapMode: Text.WordWrap
|
||||
|
||||
// TRANSLATORS: %1 will be replaced with the normal text color, %2 the color for the length check
|
||||
text: qsTr("<font color=\"%1\">The password needs to be </font><font color=\"%2\">least 12 characters long</font><font color=\"%1\">, contain </font><font color=\"%3\">lowercase</font><font color=\"%1\">, </font><font color=\"%4\">uppercase</font><font color=\"%1\"> letters as well as </font><font color=\"%5\">numbers</font><font color=\"%1\"> and </font><font color=\"%6\">special characters</font><font color=\"%1\">.</font>")
|
||||
.arg(app.accentColor)
|
||||
.arg(!root.isLongEnough ? "red" : app.accentColor)
|
||||
.arg(!root.hasLower ? "red" : app.accentColor)
|
||||
.arg(!root.hasUpper ? "red" : app.accentColor)
|
||||
.arg(!root.hasNumbers ? "red" : app.accentColor)
|
||||
.arg(!root.hasSpecialChar ? "red" : app.accentColor)
|
||||
font.pixelSize: app.smallFont
|
||||
}
|
||||
|
||||
TextField {
|
||||
id: confirmationPasswordTextField
|
||||
Layout.fillWidth: true
|
||||
echoMode: TextInput.Password
|
||||
placeholderText: qsTr("Confirm password")
|
||||
}
|
||||
|
||||
Label {
|
||||
Layout.fillWidth: true
|
||||
wrapMode: Text.WordWrap
|
||||
|
||||
text: root.confirmationMatches ? qsTr("<font color=\"%1\">The passwords match.</font>").arg(app.accentColor) : qsTr("<font color=\"%1\">The passwords </font><font color=\"%2\">do not</font><font color=\"%1\"> match.</font>").arg(app.accentColor).arg("red")
|
||||
font.pixelSize: app.smallFont
|
||||
}
|
||||
}
|
||||
14
nymea-app/ui/components/BusyOverlay.qml
Normal file
14
nymea-app/ui/components/BusyOverlay.qml
Normal file
@ -0,0 +1,14 @@
|
||||
import QtQuick 2.9
|
||||
import QtQuick.Controls 2.1
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
color: "#55000000"
|
||||
visible: shown
|
||||
|
||||
property bool shown: false
|
||||
|
||||
BusyIndicator {
|
||||
anchors.centerIn: parent
|
||||
}
|
||||
}
|
||||
@ -1,96 +0,0 @@
|
||||
import QtQuick 2.9
|
||||
import QtQuick.Controls 2.2
|
||||
import QtQuick.Layouts 1.3
|
||||
import Nymea 1.0
|
||||
import "../components"
|
||||
|
||||
Page {
|
||||
id: root
|
||||
header: GuhHeader {
|
||||
text: qsTr("Cloud login")
|
||||
onBackPressed: pageStack.pop()
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
anchors { left: parent.left; top: parent.top; right: parent.right }
|
||||
visible: Engine.awsClient.isLoggedIn
|
||||
Label {
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: app.margins
|
||||
Layout.leftMargin: app.margins
|
||||
Layout.rightMargin: app.margins
|
||||
wrapMode: Text.WordWrap
|
||||
text: qsTr("Logged in as %1").arg(Engine.awsClient.username)
|
||||
}
|
||||
|
||||
Button {
|
||||
Layout.fillWidth: true
|
||||
Layout.margins: app.margins
|
||||
text: qsTr("Log out")
|
||||
onClicked: Engine.awsClient.logout();
|
||||
}
|
||||
|
||||
ThinDivider {}
|
||||
|
||||
Label {
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: app.margins
|
||||
Layout.leftMargin: app.margins
|
||||
Layout.rightMargin: app.margins
|
||||
wrapMode: Text.WordWrap
|
||||
text: Engine.awsClient.awsDevices.count === 0 ?
|
||||
qsTr("There are no boxes connected to your cloud yet.") :
|
||||
qsTr("There (are|is) %1 boxe(s) connected to your cloud", "", Engine.awsClient.awsDevices.count)
|
||||
}
|
||||
Repeater {
|
||||
model: Engine.awsClient.awsDevices
|
||||
delegate: MeaListItemDelegate {
|
||||
Layout.fillWidth: true
|
||||
text: model.name
|
||||
subText: model.uuid
|
||||
progressive: false
|
||||
prominentSubText: false
|
||||
iconName: "../images/cloud.svg"
|
||||
secondaryIconName: model.online ? "../images/cloud.svg" : "../images/cloud-offline.svg"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
anchors { left: parent.left; right: parent.right; top: parent.top }
|
||||
visible: !Engine.awsClient.isLoggedIn
|
||||
Label {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: app.margins; Layout.rightMargin: app.margins; Layout.topMargin: app.margins
|
||||
text: "Username (e-mail)"
|
||||
}
|
||||
TextField {
|
||||
id: usernameTextField
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: app.margins; Layout.rightMargin: app.margins
|
||||
placeholderText: "john@dummy.com"
|
||||
inputMethodHints: Qt.ImhEmailCharactersOnly
|
||||
}
|
||||
Label {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: app.margins; Layout.rightMargin: app.margins; Layout.topMargin: app.margins
|
||||
text: qsTr("Password")
|
||||
}
|
||||
TextField {
|
||||
id: passwordTextField
|
||||
Layout.leftMargin: app.margins; Layout.rightMargin: app.margins
|
||||
Layout.fillWidth: true
|
||||
echoMode: TextInput.Password
|
||||
}
|
||||
|
||||
Button {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: app.margins; Layout.rightMargin: app.margins; Layout.topMargin: app.margins
|
||||
text: qsTr("OK")
|
||||
enabled: usernameTextField.displayText.length > 0 && passwordTextField.displayText.length > 0
|
||||
onClicked: {
|
||||
Engine.awsClient.login(usernameTextField.text, passwordTextField.text);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -24,6 +24,14 @@ Page {
|
||||
}
|
||||
}
|
||||
|
||||
function connectToHost(url) {
|
||||
Engine.connection.connect(url)
|
||||
var page = pageStack.push(Qt.resolvedUrl("ConnectingPage.qml"))
|
||||
page.cancel.connect(function() {
|
||||
Engine.connection.disconnect()
|
||||
})
|
||||
}
|
||||
|
||||
NymeaDiscovery {
|
||||
id: discovery
|
||||
objectName: "discovery"
|
||||
@ -87,15 +95,11 @@ Page {
|
||||
ListElement { iconSource: "../images/network-vpn.svg"; text: qsTr("Manual connection"); page: "ManualConnectPage.qml" }
|
||||
ListElement { iconSource: "../images/bluetooth.svg"; text: qsTr("Wireless setup"); page: "BluetoothDiscoveryPage.qml"; }
|
||||
ListElement { iconSource: "../images/private-browsing.svg"; text: qsTr("Demo mode"); page: "" }
|
||||
ListElement { iconSource: "../images/stock_application.svg"; text: qsTr("App settings"); page: "../AppSettingsPage.qml" }
|
||||
ListElement { iconSource: "../images/stock_application.svg"; text: qsTr("App settings"); page: "../appsettings/AppSettingsPage.qml" }
|
||||
}
|
||||
onClicked: {
|
||||
if (index === 2) {
|
||||
Engine.connection.connect("nymea://nymea.nymea.io:2222")
|
||||
var page = pageStack.push(Qt.resolvedUrl("ConnectingPage.qml"))
|
||||
page.cancel.connect(function() {
|
||||
Engine.connection.disconnect()
|
||||
})
|
||||
root.connectToHost("nymea://nymea.nymea.io:2222")
|
||||
} else {
|
||||
pageStack.push(model.get(index).page);
|
||||
}
|
||||
@ -162,11 +166,12 @@ Page {
|
||||
var bearerPreference = [Connection.BearerTypeEthernet, Connection.BearerTypeWifi, Connection.BearerTypeBluetooth, Connection.BearerTypeCloud]
|
||||
var oldBearerPriority = bearerPreference.indexOf(oldConfig.bearerType);
|
||||
var newBearerPriority = bearerPreference.indexOf(newConfig.bearerType);
|
||||
if (newBearerPriority > oldBearerPriority) {
|
||||
if (newBearerPriority < oldBearerPriority) {
|
||||
print(discoveryDevice.name, "switching to preferred index", i, "of bearer type", newConfig.bearerType, "from", oldConfig.bearerType, "new prio:", newBearerPriority, "old:", oldBearerPriority)
|
||||
usedConfigIndex = i;
|
||||
continue;
|
||||
}
|
||||
if (oldBearerPriority > newBearerPriority) {
|
||||
if (oldBearerPriority < newBearerPriority) {
|
||||
continue; // discard new one the one we have is on a better bearer type
|
||||
}
|
||||
|
||||
@ -208,16 +213,15 @@ Page {
|
||||
progressive: false
|
||||
property bool isSecure: discoveryDevice.connections.get(defaultConnectionIndex).secure
|
||||
property bool isTrusted: Engine.connection.isTrusted(discoveryDeviceDelegate.discoveryDevice.connections.get(defaultConnectionIndex).url)
|
||||
secondaryIconName: isSecure ? "../images/network-secure.svg" : ""
|
||||
secondaryIconColor: isTrusted ? app.accentColor : Material.foreground
|
||||
property bool isOnline: discoveryDevice.connections.get(defaultConnectionIndex).online
|
||||
tertiaryIconName: isSecure ? "../images/network-secure.svg" : ""
|
||||
tertiaryIconColor: isTrusted ? app.accentColor : Material.foreground
|
||||
secondaryIconName: !isOnline ? "../images/cloud-error.svg" : ""
|
||||
secondaryIconColor: "red"
|
||||
swipe.enabled: discoveryDeviceDelegate.discoveryDevice.deviceType === DiscoveryDevice.DeviceTypeNetwork
|
||||
|
||||
onClicked: {
|
||||
Engine.connection.connect(discoveryDeviceDelegate.discoveryDevice.connections.get(defaultConnectionIndex).url)
|
||||
var page = pageStack.push(Qt.resolvedUrl("ConnectingPage.qml"))
|
||||
page.cancel.connect(function() {
|
||||
Engine.connection.disconnect()
|
||||
})
|
||||
root.connectToHost(discoveryDeviceDelegate.discoveryDevice.connections.get(defaultConnectionIndex).url)
|
||||
}
|
||||
|
||||
swipe.right: MouseArea {
|
||||
@ -279,7 +283,7 @@ Page {
|
||||
Layout.rightMargin: app.margins
|
||||
text: qsTr("Cloud login")
|
||||
visible: !Engine.awsClient.isLoggedIn
|
||||
onClicked: pageStack.push(Qt.resolvedUrl("CloudLoginPage.qml"))
|
||||
onClicked: pageStack.push(Qt.resolvedUrl("../appsettings/CloudLoginPage.qml"))
|
||||
}
|
||||
|
||||
Button {
|
||||
@ -287,14 +291,10 @@ Page {
|
||||
Layout.leftMargin: app.margins
|
||||
Layout.rightMargin: app.margins
|
||||
Layout.bottomMargin: app.margins
|
||||
// visible: discovery.discoveryModel.count === 0
|
||||
visible: discovery.discoveryModel.count === 0
|
||||
text: qsTr("Demo mode (online)")
|
||||
onClicked: {
|
||||
Engine.connection.connect("nymea://nymea.nymea.io:2222")
|
||||
var page = pageStack.push(Qt.resolvedUrl("ConnectingPage.qml"))
|
||||
page.cancel.connect(function() {
|
||||
Engine.connection.disconnect()
|
||||
})
|
||||
root.connectToHost("nymea://nymea.nymea.io:2222")
|
||||
}
|
||||
}
|
||||
|
||||
@ -420,7 +420,7 @@ Page {
|
||||
|
||||
onAccepted: {
|
||||
Engine.connection.acceptCertificate(certDialog.url, certDialog.fingerprint)
|
||||
Engine.connection.connect(certDialog.url)
|
||||
root.connectToHost(certDialog.url)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -532,12 +532,14 @@ Page {
|
||||
return ""
|
||||
}
|
||||
|
||||
secondaryIconName: model.secure ? "../images/network-secure.svg" : ""
|
||||
secondaryIconColor: isTrusted ? app.accentColor : "gray"
|
||||
tertiaryIconName: model.secure ? "../images/network-secure.svg" : ""
|
||||
tertiaryIconColor: isTrusted ? app.accentColor : "gray"
|
||||
readonly property bool isTrusted: Engine.connection.isTrusted(url)
|
||||
secondaryIconName: !model.online ? "../images/cloud-error.svg" : ""
|
||||
secondaryIconColor: "red"
|
||||
|
||||
onClicked: {
|
||||
Engine.connection.connect(dialog.discoveryDevice.connections.get(index).url)
|
||||
root.connectToHost(dialog.discoveryDevice.connections.get(index).url)
|
||||
dialog.close()
|
||||
}
|
||||
}
|
||||
|
||||
@ -26,6 +26,13 @@ Page {
|
||||
wrapMode: Text.WordWrap
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
}
|
||||
Label {
|
||||
Layout.fillWidth: true
|
||||
text: Engine.connection.url
|
||||
font.pixelSize: app.smallFont
|
||||
wrapMode: Text.WrapAtWordBoundaryOrAnywhere
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
}
|
||||
}
|
||||
|
||||
Button {
|
||||
|
||||
@ -9,15 +9,15 @@
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
width="96"
|
||||
height="96"
|
||||
id="svg4874"
|
||||
width="90"
|
||||
height="90"
|
||||
id="svg6138"
|
||||
version="1.1"
|
||||
inkscape:version="0.91+devel r"
|
||||
viewBox="0 0 96 96.000001"
|
||||
sodipodi:docname="weather-clouds-symbolic.svg">
|
||||
viewBox="0 0 90 90.000001"
|
||||
sodipodi:docname="sync-idle.svg">
|
||||
<defs
|
||||
id="defs4876" />
|
||||
id="defs6140" />
|
||||
<sodipodi:namedview
|
||||
id="base"
|
||||
pagecolor="#ffffff"
|
||||
@ -25,91 +25,87 @@
|
||||
borderopacity="1.0"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:zoom="4.4959994"
|
||||
inkscape:cx="11.154354"
|
||||
inkscape:cy="34.719727"
|
||||
inkscape:zoom="6.3664629"
|
||||
inkscape:cx="-7.1609628"
|
||||
inkscape:cy="35.214212"
|
||||
inkscape:document-units="px"
|
||||
inkscape:current-layer="g4780"
|
||||
showgrid="false"
|
||||
showborder="true"
|
||||
inkscape:current-layer="g6253"
|
||||
showgrid="true"
|
||||
fit-margin-top="0"
|
||||
fit-margin-left="0"
|
||||
fit-margin-right="0"
|
||||
fit-margin-bottom="0"
|
||||
inkscape:snap-global="true"
|
||||
inkscape:snap-bbox="true"
|
||||
inkscape:bbox-paths="true"
|
||||
inkscape:bbox-nodes="true"
|
||||
inkscape:snap-bbox-edge-midpoints="true"
|
||||
inkscape:snap-bbox-midpoints="true"
|
||||
inkscape:snap-bbox-edge-midpoints="true"
|
||||
inkscape:bbox-nodes="true"
|
||||
inkscape:object-paths="true"
|
||||
inkscape:snap-intersection-paths="true"
|
||||
inkscape:object-nodes="true"
|
||||
inkscape:snap-smooth-nodes="true"
|
||||
inkscape:snap-midpoints="true"
|
||||
inkscape:snap-others="true"
|
||||
inkscape:snap-object-midpoints="true"
|
||||
inkscape:snap-center="true"
|
||||
showguides="true"
|
||||
inkscape:guide-bbox="true"
|
||||
inkscape:snap-global="true">
|
||||
inkscape:guide-bbox="true">
|
||||
<inkscape:grid
|
||||
type="xygrid"
|
||||
id="grid5451"
|
||||
empspacing="8" />
|
||||
<sodipodi:guide
|
||||
orientation="1,0"
|
||||
position="8,-8.0000001"
|
||||
id="guide4063" />
|
||||
<sodipodi:guide
|
||||
orientation="1,0"
|
||||
position="4,-8.0000001"
|
||||
id="guide4065" />
|
||||
id="grid6700"
|
||||
empspacing="6" />
|
||||
<sodipodi:guide
|
||||
orientation="0,1"
|
||||
position="-8,88.000001"
|
||||
id="guide4067" />
|
||||
position="62,87"
|
||||
id="guide4084" />
|
||||
<sodipodi:guide
|
||||
orientation="0,1"
|
||||
position="-8,92.000001"
|
||||
id="guide4069" />
|
||||
position="63,84"
|
||||
id="guide4086" />
|
||||
<sodipodi:guide
|
||||
orientation="0,1"
|
||||
position="104,4"
|
||||
id="guide4071" />
|
||||
position="63,81"
|
||||
id="guide4088" />
|
||||
<sodipodi:guide
|
||||
orientation="1,0"
|
||||
position="3,70"
|
||||
id="guide4090" />
|
||||
<sodipodi:guide
|
||||
orientation="1,0"
|
||||
position="6,66"
|
||||
id="guide4092" />
|
||||
<sodipodi:guide
|
||||
orientation="1,0"
|
||||
position="9,59"
|
||||
id="guide4094" />
|
||||
<sodipodi:guide
|
||||
orientation="1,0"
|
||||
position="87,63"
|
||||
id="guide4096" />
|
||||
<sodipodi:guide
|
||||
orientation="1,0"
|
||||
position="84,64"
|
||||
id="guide4098" />
|
||||
<sodipodi:guide
|
||||
orientation="1,0"
|
||||
position="81,55"
|
||||
id="guide4100" />
|
||||
<sodipodi:guide
|
||||
orientation="0,1"
|
||||
position="-5,8.0000001"
|
||||
id="guide4073" />
|
||||
<sodipodi:guide
|
||||
orientation="1,0"
|
||||
position="88,-8.0000001"
|
||||
id="guide4077" />
|
||||
position="60,3"
|
||||
id="guide4102" />
|
||||
<sodipodi:guide
|
||||
orientation="0,1"
|
||||
position="-8,84.000001"
|
||||
id="guide4074" />
|
||||
position="61,6"
|
||||
id="guide4104" />
|
||||
<sodipodi:guide
|
||||
orientation="1,0"
|
||||
position="12,-8.0000001"
|
||||
id="guide4076" />
|
||||
<sodipodi:guide
|
||||
orientation="1,0"
|
||||
position="84,-8.0000001"
|
||||
id="guide4080" />
|
||||
<sodipodi:guide
|
||||
position="48,-8.0000001"
|
||||
orientation="1,0"
|
||||
id="guide4170" />
|
||||
<sodipodi:guide
|
||||
position="-8,48"
|
||||
orientation="0,1"
|
||||
id="guide4172" />
|
||||
<sodipodi:guide
|
||||
position="92,-8.0000001"
|
||||
orientation="1,0"
|
||||
id="guide4760" />
|
||||
position="62,9"
|
||||
id="guide4106" />
|
||||
</sodipodi:namedview>
|
||||
<metadata
|
||||
id="metadata4879">
|
||||
id="metadata6143">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
@ -124,37 +120,25 @@
|
||||
inkscape:label="Layer 1"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1"
|
||||
transform="translate(67.857146,-78.50504)">
|
||||
transform="translate(-283.57144,-358.79068)">
|
||||
<g
|
||||
transform="matrix(0,-1,-1,0,373.50506,516.50504)"
|
||||
id="g4845"
|
||||
style="display:inline">
|
||||
<g
|
||||
inkscape:export-ydpi="90"
|
||||
inkscape:export-xdpi="90"
|
||||
inkscape:export-filename="next01.png"
|
||||
transform="matrix(-0.9996045,0,0,1,575.94296,-611.00001)"
|
||||
id="g4778"
|
||||
inkscape:label="Layer 1">
|
||||
<g
|
||||
transform="matrix(-1,0,0,1,575.99999,611)"
|
||||
id="g4780"
|
||||
style="display:inline">
|
||||
<rect
|
||||
style="color:#000000;display:inline;overflow:visible;visibility:visible;fill:none;stroke:none;stroke-width:4;marker:none;enable-background:accumulate"
|
||||
id="rect4782"
|
||||
width="96.037987"
|
||||
height="96"
|
||||
x="-438.00244"
|
||||
y="345.36221"
|
||||
transform="scale(-1,1)" />
|
||||
<path
|
||||
style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;line-height:normal;font-family:sans-serif;-inkscape-font-specification:sans-serif;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;baseline-shift:baseline;text-anchor:start;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;color-interpolation:sRGB;color-interpolation-filters:linearRGB;fill:#808080;fill-opacity:1;stroke:none;stroke-width:4.00000048;marker:none;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
|
||||
d="M 51.539062 16.888672 C 39.655652 16.888672 29.559451 24.212821 24.738281 34.675781 L 24.416016 35.376953 L 23.646484 35.433594 C 12.678934 36.232468 3.9980469 45.714566 3.9980469 57.255859 C 3.9980469 69.316737 13.439055 79.21875 25.109375 79.21875 L 51.539062 79.21875 L 76.175781 79.21875 C 84.926381 79.21875 92 71.787763 92 62.779297 C 92 55.94799 87.899411 50.217256 82.082031 47.746094 L 81.324219 47.423828 L 81.287109 46.603516 C 80.540729 30.134022 67.596276 16.888672 51.541016 16.888672 L 51.539062 16.888672 z M 51.539062 20.888672 C 65.393253 20.888672 76.634896 32.305371 77.291016 46.783203 L 77.289062 46.779297 L 77.4375 50.117188 L 80.515625 51.427734 C 84.906955 53.293126 87.998047 57.547827 87.998047 62.779297 C 87.998047 69.694971 82.658528 75.21875 76.173828 75.21875 L 51.539062 75.21875 L 25.109375 75.21875 C 15.702915 75.21875 7.9980469 67.230513 7.9980469 57.255859 C 7.9980469 47.719033 15.103887 40.06517 23.935547 39.421875 L 23.9375 39.421875 L 27.064453 39.191406 L 28.371094 36.349609 C 32.601004 27.169791 41.300243 20.888672 51.539062 20.888672 z "
|
||||
transform="matrix(0,-1,-1.0003957,0,438.00245,441.36222)"
|
||||
id="path4181" />
|
||||
</g>
|
||||
</g>
|
||||
id="g6253"
|
||||
inkscape:export-filename="planemode01.png"
|
||||
inkscape:export-xdpi="90"
|
||||
inkscape:export-ydpi="90"
|
||||
transform="matrix(-1,0,0,1,547.57143,-1341.5715)">
|
||||
<rect
|
||||
style="fill:none;stroke:none"
|
||||
id="rect6257"
|
||||
width="90"
|
||||
height="90"
|
||||
x="174"
|
||||
y="1700.3622" />
|
||||
<path
|
||||
style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;font-variant-ligatures:none;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-feature-settings:normal;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;baseline-shift:baseline;text-anchor:start;white-space:normal;shape-padding:0;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#808080;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:4;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
|
||||
d="m 212.5,1714.3613 c -11.29815,0 -20.5,9.2019 -20.5,20.5 l 0,0.01 0,0.01 c 0.002,0.2497 0.0454,0.4967 0.0566,0.7461 -10.15029,1.2279 -18.0494,9.7961 -18.05664,20.2363 0,11.2982 9.20185,20.5 20.5,20.5 l 49,0 c 11.29815,0 20.5,-9.2018 20.5,-20.5 l 0,-0 c -0.0116,-8.9301 -5.844,-16.696 -14.21094,-19.4063 0.0644,-0.5317 0.21041,-1.0527 0.21094,-1.5898 l 0,-0 c 0,-7.4321 -6.06785,-13.5 -13.5,-13.5 l -0.002,0 -0.002,0 c -2.55665,0 -4.93658,0.9271 -7.07421,2.2754 -3.76777,-5.6712 -10.02107,-9.2653 -16.91993,-9.2754 l -0.002,0 z m -0.002,4 0.002,0 c 6.1355,0.01 11.75025,3.4089 14.59766,8.8418 l 1.18945,2.2676 1.91211,-1.7031 c 1.73553,-1.5457 3.97683,-2.4018 6.30078,-2.4063 5.27039,0 9.5,4.2296 9.5,9.5 -0.001,0.8309 -0.11279,1.6591 -0.33008,2.4629 l -0.54297,2.0117 2.03125,0.461 c 7.51333,1.7082 12.82999,8.3597 12.8418,16.0644 0,7e-4 0,0 0,0 0,6e-4 0,0 0,0 -0.002,9.1346 -7.36493,16.4961 -16.5,16.4961 l -49,0 c -9.13573,0 -16.49893,-7.3625 -16.5,-16.498 l 0,-0 c 0.007,-9.023 7.2045,-16.3348 16.22656,-16.4843 l 2.27539,-0.037 -0.33007,-2.25 c -0.10801,-0.7397 -0.16511,-1.4866 -0.17188,-2.2343 0.003,-9.133 7.36456,-16.4931 16.49805,-16.4942 z"
|
||||
id="path4154"
|
||||
inkscape:connector-curvature="0" />
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 6.3 KiB After Width: | Height: | Size: 6.0 KiB |
@ -11,21 +11,16 @@ Page {
|
||||
onBackPressed: pageStack.pop();
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: Engine.basicConfiguration
|
||||
onCloudEnabledChanged: {
|
||||
if (Engine.jsonRpcClient.cloudConnectionState === JsonRpcClient.CloudConnectionStateUnconfigured) {
|
||||
Engine.deployCertificate();
|
||||
}
|
||||
}
|
||||
}
|
||||
Item {
|
||||
id: d
|
||||
property bool deploymentStarted: false
|
||||
|
||||
Connections {
|
||||
target: Engine.jsonRpcClient
|
||||
onCloudConnectionStateChanged: {
|
||||
if (Engine.awsClient.isLoggedIn && Engine.awsClient.awsDevices.getDevice(Engine.jsonRpcClient.serverUuid) === null) {
|
||||
print("Pairing user and box...")
|
||||
Engine.jsonRpcClient.setupRemoteAccess(Engine.awsClient.idToken, Engine.awsClient.userId);
|
||||
Connections {
|
||||
target: Engine.jsonRpcClient
|
||||
onCloudConnectionStateChanged: {
|
||||
if (Engine.jsonRpcClient.cloudConnectionState == JsonRpcClient.CloudConnectionStateConnected) {
|
||||
d.deploymentStarted = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -51,33 +46,67 @@ Page {
|
||||
}
|
||||
}
|
||||
|
||||
ThinDivider {}
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: app.margins
|
||||
Layout.rightMargin: app.margins
|
||||
visible: Engine.basicConfiguration.cloudEnabled
|
||||
|
||||
BusyIndicator {
|
||||
visible: Engine.jsonRpcClient.cloudConnectionState == JsonRpcClient.CloudConnectionStateUnconfigured ||
|
||||
Engine.jsonRpcClient.cloudConnectionState == JsonRpcClient.CloudConnectionStateConnecting
|
||||
ColorIcon {
|
||||
Layout.preferredHeight: busyIndicator.height
|
||||
Layout.preferredWidth: height
|
||||
name: Engine.jsonRpcClient.cloudConnectionState === JsonRpcClient.CloudConnectionStateConnected
|
||||
? "../images/cloud.svg"
|
||||
: Engine.jsonRpcClient.cloudConnectionState === JsonRpcClient.CloudConnectionStateUnconfigured
|
||||
? "../images/cloud-error.svg"
|
||||
: "../images/cloud-offline.svg"
|
||||
}
|
||||
|
||||
Label {
|
||||
Layout.fillWidth: true
|
||||
wrapMode: Text.WordWrap
|
||||
text: {
|
||||
switch (Engine.jsonRpcClient.cloudConnectionState) {
|
||||
case JsonRpcClient.CloudConnectionStateDisabled:
|
||||
return ""
|
||||
return qsTr("This box is not connected to %1:cloud").arg(app.systemName)
|
||||
case JsonRpcClient.CloudConnectionStateUnconfigured:
|
||||
return qsTr("Configuring the box to connect to nymea:cloud...");
|
||||
if (d.deploymentStarted) {
|
||||
return qsTr("Registering box in %1:cloud...").arg(app.systemName)
|
||||
}
|
||||
return qsTr("This box is not configured to connect to %1:cloud.").arg(app.systemName);
|
||||
case JsonRpcClient.CloudConnectionStateConnecting:
|
||||
return qsTr("Connecting the box to nymea:cloud...");
|
||||
return qsTr("Connecting the box to %1:cloud...").arg(app.systemName);
|
||||
case JsonRpcClient.CloudConnectionStateConnected:
|
||||
return qsTr("The box is connected to nymea:cloud.");
|
||||
return qsTr("The box is connected to %1:cloud.").arg(app.systemName);
|
||||
}
|
||||
return Engine.jsonRpcClient.cloudConnectionState
|
||||
}
|
||||
}
|
||||
BusyIndicator {
|
||||
id: busyIndicator
|
||||
visible: (Engine.jsonRpcClient.cloudConnectionState == JsonRpcClient.CloudConnectionStateUnconfigured && d.deploymentStarted) ||
|
||||
Engine.jsonRpcClient.cloudConnectionState == JsonRpcClient.CloudConnectionStateConnecting
|
||||
}
|
||||
}
|
||||
|
||||
Label {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: app.margins; Layout.rightMargin: app.margins
|
||||
visible: Engine.jsonRpcClient.cloudConnectionState === JsonRpcClient.CloudConnectionStateUnconfigured && !d.deploymentStarted
|
||||
text: qsTr("This box is not configured to access the %1:cloud. In order for a box to connect to %1:cloud it needs to be registered first.").arg(app.systemName)
|
||||
wrapMode: Text.WordWrap
|
||||
}
|
||||
|
||||
Button {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: app.margins; Layout.rightMargin: app.margins
|
||||
visible: Engine.jsonRpcClient.cloudConnectionState === JsonRpcClient.CloudConnectionStateUnconfigured && !d.deploymentStarted
|
||||
text: qsTr("Register box")
|
||||
onClicked: {
|
||||
d.deploymentStarted = true
|
||||
Engine.deployCertificate();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@ -1 +1 @@
|
||||
Subproject commit f6e2d9b3b208362159dd22a4eb11527db25d8760
|
||||
Subproject commit 3b97bc60bbaf1ae5d4ced7eb1586d2d20fe5b0ab
|
||||
Reference in New Issue
Block a user