remote connection support done

This commit is contained in:
Michael Zanetti 2018-08-31 14:42:36 +02:00
parent e9390dcb5a
commit ce06ffbcce
29 changed files with 1455 additions and 346 deletions

View File

@ -11,11 +11,10 @@
#include "qmqtt.h" #include "qmqtt.h"
#include "sigv4utils.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\ static QByteArray rootCA = "-----BEGIN CERTIFICATE-----\n\
MIIE0zCCA7ugAwIBAgIQGNrRniZ96LtKIVjNzGs7SjANBgkqhkiG9w0BAQUFADCB\n\ MIIE0zCCA7ugAwIBAgIQGNrRniZ96LtKIVjNzGs7SjANBgkqhkiG9w0BAQUFADCB\n\
yjELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDlZlcmlTaWduLCBJbmMuMR8wHQYDVQQL\n\ yjELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDlZlcmlTaWduLCBJbmMuMR8wHQYDVQQL\n\
@ -51,6 +50,31 @@ AWSClient::AWSClient(QObject *parent) : QObject(parent),
{ {
m_nam = new QNetworkAccessManager(this); 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; QSettings settings;
settings.beginGroup("cloud"); settings.beginGroup("cloud");
m_username = settings.value("username").toString(); m_username = settings.value("username").toString();
@ -59,6 +83,7 @@ AWSClient::AWSClient(QObject *parent) : QObject(parent),
m_accessTokenExpiry = settings.value("accessTokenExpiry").toDateTime(); m_accessTokenExpiry = settings.value("accessTokenExpiry").toDateTime();
m_idToken = settings.value("idToken").toByteArray(); m_idToken = settings.value("idToken").toByteArray();
m_refreshToken = settings.value("refreshToken").toByteArray(); m_refreshToken = settings.value("refreshToken").toByteArray();
m_confirmationPending = settings.value("confirmationPending", false).toBool();
m_identityId = settings.value("identityId").toByteArray(); m_identityId = settings.value("identityId").toByteArray();
@ -88,16 +113,22 @@ AWSDevices *AWSClient::awsDevices() const
return m_devices; return m_devices;
} }
bool AWSClient::confirmationPending() const
{
return m_confirmationPending;
}
void AWSClient::login(const QString &username, const QString &password) void AWSClient::login(const QString &username, const QString &password)
{ {
m_username = username; m_username = username;
// Ok... Please fogive me for this... AWS APIs are just unbearable... can't be bothered // Due to an issue in AWS apis it's very complex to use the refresh token. Taking a shortcut here for now:
// any more to walk through another chain of calls in order to have the refreshToken working.
// Will store the password in the config for now and re-login when the accessToken expires. // 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 // 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; 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; QUrlQuery query;
query.addQueryItem("Action", "InitiateAuth"); query.addQueryItem("Action", "InitiateAuth");
@ -106,12 +137,12 @@ void AWSClient::login(const QString &username, const QString &password)
QNetworkRequest request(url); QNetworkRequest request(url);
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-amz-json-1.0"); 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"); request.setRawHeader("X-Amz-Target", "AWSCognitoIdentityProviderService.InitiateAuth");
QVariantMap params; QVariantMap params;
params.insert("AuthFlow", "USER_PASSWORD_AUTH"); params.insert("AuthFlow", "USER_PASSWORD_AUTH");
params.insert("ClientId", clientId); params.insert("ClientId", m_configs.at(m_usedConfigIndex).clientId);
QVariantMap authParams; QVariantMap authParams;
authParams.insert("USERNAME", username); 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(); qWarning() << "Error logging in to aws:" << reply->error() << reply->errorString();
m_username.clear(); m_username.clear();
m_password.clear(); m_password.clear();
emit loginResult(LoginErrorUnknownError);
return; return;
} }
QByteArray data = reply->readAll(); 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(); qWarning() << "Failed to parse AWS login response" << error.errorString();
m_username.clear(); m_username.clear();
m_password.clear(); m_password.clear();
emit loginResult(LoginErrorUnknownError);
return; return;
} }
@ -160,7 +193,7 @@ void AWSClient::login(const QString &username, const QString &password)
settings.setValue("idToken", m_idToken); settings.setValue("idToken", m_idToken);
settings.setValue("refreshToken", m_refreshToken); 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(); emit isLoggedInChanged();
qDebug() << "Getting cognito ID"; qDebug() << "Getting cognito ID";
@ -177,9 +210,310 @@ void AWSClient::logout()
emit isLoggedInChanged(); 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() 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; QUrlQuery query;
query.addQueryItem("Action", "GetId"); query.addQueryItem("Action", "GetId");
@ -188,19 +522,21 @@ void AWSClient::getId()
QNetworkRequest request(url); QNetworkRequest request(url);
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-amz-json-1.0"); 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"); request.setRawHeader("X-Amz-Target", "AWSCognitoIdentityService.GetId");
QVariantMap logins; 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; 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); params.insert("Logins", logins);
QJsonDocument jsonDoc = QJsonDocument::fromVariant(params); QJsonDocument jsonDoc = QJsonDocument::fromVariant(params);
QByteArray payload = jsonDoc.toJson(QJsonDocument::Compact); QByteArray payload = jsonDoc.toJson(QJsonDocument::Compact);
// qDebug() << "Posting:" << request.url().toString();
// qDebug() << "Payload:" << payload;
QNetworkReply *reply = m_nam->post(request, payload); QNetworkReply *reply = m_nam->post(request, payload);
connect(reply, &QNetworkReply::finished, this, [this, reply]() { connect(reply, &QNetworkReply::finished, this, [this, reply]() {
reply->deleteLater(); reply->deleteLater();
@ -220,7 +556,7 @@ void AWSClient::getId()
settings.beginGroup("cloud"); settings.beginGroup("cloud");
settings.setValue("identityId", m_identityId); settings.setValue("identityId", m_identityId);
qDebug() << "Received cognito identity id" << m_identityId; qDebug() << "Received cognito identity id" << m_identityId;// << qUtf8Printable(data);
getCredentialsForIdentity(m_identityId); getCredentialsForIdentity(m_identityId);
}); });
@ -240,13 +576,13 @@ void AWSClient::fetchCertificate(const QString &uuid, std::function<void(const Q
{ {
QString fixedUuid = uuid; QString fixedUuid = uuid;
fixedUuid.remove(QRegExp("[{}]")); fixedUuid.remove(QRegExp("[{}]"));
QNetworkRequest request(QUrl("https://testproductionservice-cloud.guh.io/certificatews/certificate")); QNetworkRequest request(m_configs.at(m_usedConfigIndex).certificateEndpoint);
request.setRawHeader("X-api-key", "BJMq4h19dB5yjVKwagTvk9u72FqLecEoWPJIkyDj"); request.setRawHeader("X-api-key", m_configs.at(m_usedConfigIndex).certificateApiKey.toUtf8());
request.setRawHeader("X-api-vendorId", "testVendor001"); request.setRawHeader("X-api-vendorId", m_configs.at(m_usedConfigIndex).certificateVendorId.toUtf8());
request.setRawHeader("X-api-deviceId", fixedUuid.toUtf8()); request.setRawHeader("X-api-deviceId", fixedUuid.toUtf8());
request.setRawHeader("X-api-serialId", "69696969"); request.setRawHeader("X-api-serialId", "69696969");
QNetworkReply *reply = m_nam->get(request); QNetworkReply *reply = m_nam->get(request);
connect(reply, &QNetworkReply::finished, this, [reply, callback]() { connect(reply, &QNetworkReply::finished, this, [this, reply, callback]() {
reply->deleteLater(); reply->deleteLater();
QByteArray data = reply->readAll(); QByteArray data = reply->readAll();
if (reply->error() != QNetworkReply::NoError) { 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() << "Certificate received" << certificate;
qDebug() << "Public key" << publicKey; qDebug() << "Public key" << publicKey;
qDebug() << "Private key" << privateKey; 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) 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; QUrlQuery query;
query.addQueryItem("Action", "GetCredentialsForIdentity"); query.addQueryItem("Action", "GetCredentialsForIdentity");
@ -282,11 +633,11 @@ void AWSClient::getCredentialsForIdentity(const QString &identityId)
QNetworkRequest request(url); QNetworkRequest request(url);
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-amz-json-1.0"); 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"); request.setRawHeader("X-Amz-Target", "AWSCognitoIdentityService.GetCredentialsForIdentity");
QVariantMap logins; 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; QVariantMap params;
params.insert("IdentityId", identityId); params.insert("IdentityId", identityId);
@ -295,18 +646,19 @@ void AWSClient::getCredentialsForIdentity(const QString &identityId)
QJsonDocument jsonDoc = QJsonDocument::fromVariant(params); QJsonDocument jsonDoc = QJsonDocument::fromVariant(params);
QByteArray payload = jsonDoc.toJson(QJsonDocument::Compact); QByteArray payload = jsonDoc.toJson(QJsonDocument::Compact);
qDebug() << "Calling GetCredentialsForIdentity:" << request.url(); // qDebug() << "Calling GetCredentialsForIdentity:" << request.url();
qDebug() << "Headers:"; // qDebug() << "Headers:";
foreach (const QByteArray &headerName, request.rawHeaderList()) { // foreach (const QByteArray &headerName, request.rawHeaderList()) {
qDebug() << headerName << ":" << request.rawHeader(headerName); // qDebug() << headerName << ":" << request.rawHeader(headerName);
} // }
qDebug() << "Payload:" << qUtf8Printable(payload); // qDebug() << "Payload:" << qUtf8Printable(payload);
QNetworkReply *reply = m_nam->post(request, payload); QNetworkReply *reply = m_nam->post(request, payload);
connect(reply, &QNetworkReply::finished, this, [this, reply]() { connect(reply, &QNetworkReply::finished, this, [this, reply]() {
reply->deleteLater(); reply->deleteLater();
if (reply->error() != QNetworkReply::NoError) { if (reply->error() != QNetworkReply::NoError) {
qWarning() << "Error calling GetCredentialsForIdentity" << reply->errorString(); qWarning() << "Error calling GetCredentialsForIdentity" << reply->errorString();
emit loginResult(LoginErrorUnknownError);
return; return;
} }
QByteArray data = reply->readAll(); QByteArray data = reply->readAll();
@ -314,6 +666,7 @@ void AWSClient::getCredentialsForIdentity(const QString &identityId)
QJsonDocument jsonDoc = QJsonDocument::fromJson(data, &error); QJsonDocument jsonDoc = QJsonDocument::fromJson(data, &error);
if (error.error != QJsonParseError::NoError) { if (error.error != QJsonParseError::NoError) {
qWarning() << "Error parsing JSON reply from GetCredentialsForIdentity" << error.errorString(); qWarning() << "Error parsing JSON reply from GetCredentialsForIdentity" << error.errorString();
emit loginResult(LoginErrorUnknownError);
return; return;
} }
QVariantMap credentialsMap = jsonDoc.toVariant().toMap().value("Credentials").toMap(); 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("sessionToken", m_sessionToken);
settings.setValue("sessionTokenExpiry", m_sessionTokenExpiry); 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()) { while (!m_callQueue.isEmpty()) {
QueuedCall qc = m_callQueue.takeFirst(); 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)); m_callQueue.append(QueuedCall("postToMQTT", boxId, callback));
return true; // So far it looks we're doing ok... let's return true 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)); QString topic = QString("%1/%2/proxy").arg(boxId).arg(QString(m_identityId));
// This is somehow broken in AWS... // 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); 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("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 // 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() << "Posting to MQTT:" << request.url().toString();
qDebug() << "HEADERS:"; qDebug() << "HEADERS:";
@ -430,7 +783,7 @@ void AWSClient::fetchDevices()
return; return;
} }
qDebug() << "Fetching cloud devices"; 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); QNetworkRequest request(url);
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
request.setRawHeader("x-api-idToken", m_idToken); 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... // 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; QUrlQuery query;
query.addQueryItem("Action", "InitiateAuth"); query.addQueryItem("Action", "InitiateAuth");
@ -492,12 +846,12 @@ void AWSClient::refreshAccessToken()
QNetworkRequest request(url); QNetworkRequest request(url);
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-amz-json-1.0"); 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"); request.setRawHeader("X-Amz-Target", "AWSCognitoIdentityProviderService.InitiateAuth");
QVariantMap params; QVariantMap params;
params.insert("AuthFlow", "REFRESH_TOKEN_AUTH"); params.insert("AuthFlow", "REFRESH_TOKEN_AUTH");
params.insert("ClientId", clientId); params.insert("ClientId", m_configs.at(m_usedConfigIndex).clientId);
QVariantMap authParams; QVariantMap authParams;
authParams.insert("REFRESH_TOKEN", m_refreshToken); authParams.insert("REFRESH_TOKEN", m_refreshToken);
@ -581,7 +935,7 @@ QHash<int, QByteArray> AWSDevices::roleNames() const
AWSDevice *AWSDevices::getDevice(const QString &uuid) const AWSDevice *AWSDevices::getDevice(const QString &uuid) const
{ {
for (int i = 0; i < m_list.count(); i++) { 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); return m_list.at(i);
} }
} }
@ -605,6 +959,25 @@ void AWSDevices::insert(AWSDevice *device)
emit countChanged(); 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): AWSDevice::AWSDevice(const QString &id, const QString &name, bool online, QObject *parent):
QObject (parent), QObject (parent),
m_id(id), m_id(id),

View File

@ -46,31 +46,66 @@ public:
Q_INVOKABLE AWSDevice* getDevice(const QString &uuid) const; Q_INVOKABLE AWSDevice* getDevice(const QString &uuid) const;
Q_INVOKABLE AWSDevice* get(int index) const; Q_INVOKABLE AWSDevice* get(int index) const;
void insert(AWSDevice *device); void insert(AWSDevice *device);
void remove(const QString &uuid);
signals: signals:
void countChanged(); void countChanged();
private: private:
QList<AWSDevice*> m_list; 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 class AWSClient : public QObject
{ {
Q_OBJECT Q_OBJECT
Q_PROPERTY(bool isLoggedIn READ isLoggedIn NOTIFY isLoggedInChanged) Q_PROPERTY(bool isLoggedIn READ isLoggedIn NOTIFY isLoggedInChanged)
Q_PROPERTY(QString username READ username 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 userId READ userId NOTIFY isLoggedInChanged)
Q_PROPERTY(QByteArray idToken READ idToken NOTIFY isLoggedInChanged) Q_PROPERTY(QByteArray idToken READ idToken NOTIFY isLoggedInChanged)
Q_PROPERTY(AWSDevices* awsDevices READ awsDevices CONSTANT) Q_PROPERTY(AWSDevices* awsDevices READ awsDevices CONSTANT)
Q_PROPERTY(int config READ config WRITE setConfig NOTIFY configChanged)
public: public:
enum LoginError {
LoginErrorNoError,
LoginErrorInvalidUserOrPass,
LoginErrorInvalidCode,
LoginErrorUserExists,
LoginErrorLimitExceeded,
LoginErrorUnknownError
};
Q_ENUM(LoginError)
explicit AWSClient(QObject *parent = nullptr); explicit AWSClient(QObject *parent = nullptr);
bool isLoggedIn() const; bool isLoggedIn() const;
QString username() const; QString username() const;
QByteArray userId() const; QByteArray userId() const;
AWSDevices* awsDevices() const; AWSDevices* awsDevices() const;
bool confirmationPending() const;
Q_INVOKABLE void login(const QString &username, const QString &password); Q_INVOKABLE void login(const QString &username, const QString &password);
Q_INVOKABLE void logout(); 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(); 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); 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: signals:
void loginResult(LoginError error);
void signupResult(LoginError error);
void confirmationResult(LoginError error);
void forgotPasswordResult(LoginError error);
void confirmForgotPasswordResult(LoginError error);
void isLoggedInChanged(); void isLoggedInChanged();
void confirmationPendingChanged();
void devicesFetched(); void devicesFetched();
void configChanged();
private: private:
void refreshAccessToken(); void refreshAccessToken();
void getCredentialsForIdentity(const QString &identityId); void getCredentialsForIdentity(const QString &identityId);
@ -96,9 +143,12 @@ private:
private: private:
QNetworkAccessManager *m_nam = nullptr; QNetworkAccessManager *m_nam = nullptr;
QString m_userId;
QString m_username; QString m_username;
QString m_password; QString m_password;
bool m_confirmationPending = false;
QByteArray m_accessToken; QByteArray m_accessToken;
QDateTime m_accessTokenExpiry; QDateTime m_accessTokenExpiry;
QByteArray m_idToken; QByteArray m_idToken;
@ -114,6 +164,7 @@ private:
class QueuedCall { class QueuedCall {
public: public:
QueuedCall(const QString &method): method(method) { } 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) {} QueuedCall(const QString &method, const QString &boxId, std::function<void(bool)> callback): method(method), boxId(boxId), callback(callback) {}
QString method; QString method;
QString boxId; QString boxId;
@ -122,6 +173,8 @@ private:
QList<QueuedCall> m_callQueue; QList<QueuedCall> m_callQueue;
QList<AWSConfiguration> m_configs;
int m_usedConfigIndex = 0;
AWSDevices *m_devices; AWSDevices *m_devices;
}; };

View File

@ -34,7 +34,7 @@ CloudTransport::CloudTransport(AWSClient *awsClient, QObject *parent):
QObject::connect(m_remoteproxyConnection, &RemoteProxyConnection::dataReady, this, [this](const QByteArray &data) { QObject::connect(m_remoteproxyConnection, &RemoteProxyConnection::dataReady, this, [this](const QByteArray &data) {
emit dataReady(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; qDebug() << "Remote proxy Error:" << error;
// emit NymeaTransportInterface::error(QAbstractSocket::ConnectionRefusedError); // 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) { bool postResult = m_awsClient->postToMQTT(url.host(), [this](bool success) {
if (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")); // m_remoteproxyConnection->connectServer(QUrl("wss://127.0.0.1:1212"));
} }
}); });
@ -87,9 +88,10 @@ NymeaTransportInterface::ConnectionState CloudTransport::connectionState() const
case RemoteProxyConnection::StateConnected: case RemoteProxyConnection::StateConnected:
case RemoteProxyConnection::StateAuthenticating: case RemoteProxyConnection::StateAuthenticating:
case RemoteProxyConnection::StateReady: case RemoteProxyConnection::StateReady:
case RemoteProxyConnection::StateWaitTunnel: case RemoteProxyConnection::StateAuthenticated:
return NymeaTransportInterface::ConnectionStateConnecting; return NymeaTransportInterface::ConnectionStateConnecting;
case RemoteProxyConnection::StateDisconnected: case RemoteProxyConnection::StateDisconnected:
case RemoteProxyConnection::StateDiconnecting:
return NymeaTransportInterface::ConnectionStateDisconnected; return NymeaTransportInterface::ConnectionStateDisconnected;
} }
return ConnectionStateDisconnected; return ConnectionStateDisconnected;

View File

@ -92,6 +92,8 @@ QVariant Connections::data(const QModelIndex &index, int role) const
return m_connections.at(index.row())->bearerType(); return m_connections.at(index.row())->bearerType();
case RoleSecure: case RoleSecure:
return m_connections.at(index.row())->secure(); return m_connections.at(index.row())->secure();
case RoleOnline:
return m_connections.at(index.row())->online();
} }
return QVariant(); return QVariant();
} }
@ -111,10 +113,42 @@ void Connections::addConnection(Connection *connection)
connection->setParent(this); connection->setParent(this);
beginInsertRows(QModelIndex(), m_connections.count(), m_connections.count()); beginInsertRows(QModelIndex(), m_connections.count(), m_connections.count());
m_connections.append(connection); 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(); endInsertRows();
emit countChanged(); 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 Connection* Connections::get(int index) const
{ {
if (index >= 0 && index < m_connections.count()) { if (index >= 0 && index < m_connections.count()) {
@ -130,6 +164,7 @@ QHash<int, QByteArray> Connections::roleNames() const
roles.insert(RoleBearerType, "bearerType"); roles.insert(RoleBearerType, "bearerType");
roles.insert(RoleName, "name"); roles.insert(RoleName, "name");
roles.insert(RoleSecure, "secure"); roles.insert(RoleSecure, "secure");
roles.insert(RoleOnline, "online");
return roles; return roles;
} }

View File

@ -75,7 +75,8 @@ public:
RoleUrl, RoleUrl,
RoleName, RoleName,
RoleBearerType, RoleBearerType,
RoleSecure RoleSecure,
RoleOnline
}; };
Q_ENUM(Roles) Q_ENUM(Roles)
Connections(QObject* parent = nullptr); Connections(QObject* parent = nullptr);
@ -83,6 +84,8 @@ public:
QVariant data(const QModelIndex & index, int role = Qt::DisplayRole) const override; QVariant data(const QModelIndex & index, int role = Qt::DisplayRole) const override;
void addConnection(Connection *connection); void addConnection(Connection *connection);
void removeConnection(Connection *connection);
void removeConnection(int index);
Q_INVOKABLE Connection* find(const QUrl &url) const; Q_INVOKABLE Connection* find(const QUrl &url) const;
Q_INVOKABLE Connection* get(int index) const; Q_INVOKABLE Connection* get(int index) const;

View File

@ -64,8 +64,24 @@ void DiscoveryModel::addDevice(DiscoveryDevice *device)
emit countChanged(); 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 DiscoveryDevice *DiscoveryModel::get(int index) const
{ {
if (index < 0 || index >= m_devices.count()) {
return nullptr;
}
return m_devices.at(index); return m_devices.at(index);
} }

View File

@ -46,6 +46,7 @@ public:
QVariant data(const QModelIndex & index, int role = Qt::DisplayRole) const; QVariant data(const QModelIndex & index, int role = Qt::DisplayRole) const;
void addDevice(DiscoveryDevice *device); void addDevice(DiscoveryDevice *device);
void removeDevice(DiscoveryDevice *device);
Q_INVOKABLE DiscoveryDevice *get(int index) const; Q_INVOKABLE DiscoveryDevice *get(int index) const;
Q_INVOKABLE DiscoveryDevice *find(const QUuid &uuid); Q_INVOKABLE DiscoveryDevice *find(const QUuid &uuid);

View File

@ -20,7 +20,13 @@ NymeaDiscovery::NymeaDiscovery(QObject *parent) : QObject(parent)
m_bluetooth = new BluetoothServiceDiscovery(m_discoveryModel, this); m_bluetooth = new BluetoothServiceDiscovery(m_discoveryModel, this);
#endif #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 bool NymeaDiscovery::discovering() const
@ -44,11 +50,13 @@ void NymeaDiscovery::setDiscovering(bool discovering)
syncCloudDevices(); syncCloudDevices();
Engine::instance()->awsClient()->fetchDevices(); Engine::instance()->awsClient()->fetchDevices();
} }
m_cloudPollTimer.start();
} else { } else {
m_upnp->stopDiscovery(); m_upnp->stopDiscovery();
if (m_bluetooth) { if (m_bluetooth) {
m_bluetooth->stopDiscovery(); m_bluetooth->stopDiscovery();
} }
m_cloudPollTimer.stop();
} }
emit discoveringChanged(); emit discoveringChanged();
@ -74,11 +82,31 @@ void NymeaDiscovery::syncCloudDevices()
QUrl url; QUrl url;
url.setScheme("cloud"); url.setScheme("cloud");
url.setHost(d->id()); url.setHost(d->id());
if (!device->connections()->find(url)) { Connection *conn = device->connections()->find(url);
Connection *conn = new Connection(url, Connection::BearerTypeCloud, true, d->id()); if (!conn) {
conn->setOnline(d->online()); conn = new Connection(url, Connection::BearerTypeCloud, true, d->id());
device->connections()->addConnection(conn); 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());
} }
} }

View File

@ -2,6 +2,7 @@
#define NYMEADISCOVERY_H #define NYMEADISCOVERY_H
#include <QObject> #include <QObject>
#include <QTimer>
#include "connection/awsclient.h" #include "connection/awsclient.h"
@ -10,6 +11,7 @@ class UpnpDiscovery;
class ZeroconfDiscovery; class ZeroconfDiscovery;
class BluetoothServiceDiscovery; class BluetoothServiceDiscovery;
class NymeaDiscovery : public QObject class NymeaDiscovery : public QObject
{ {
Q_OBJECT Q_OBJECT
@ -38,6 +40,8 @@ private:
ZeroconfDiscovery *m_zeroConf = nullptr; ZeroconfDiscovery *m_zeroConf = nullptr;
BluetoothServiceDiscovery *m_bluetooth = nullptr; BluetoothServiceDiscovery *m_bluetooth = nullptr;
QTimer m_cloudPollTimer;
}; };
#endif // NYMEADISCOVERY_H #endif // NYMEADISCOVERY_H

View File

@ -255,6 +255,7 @@ void UpnpDiscovery::networkReplyFinished(QNetworkReply *reply)
bool sslEnabled = url.scheme() == "nymeas" || url.scheme() == "wss"; bool sslEnabled = url.scheme() == "nymeas" || url.scheme() == "wss";
QString displayName = QString("%1:%2").arg(url.host()).arg(url.port()); QString displayName = QString("%1:%2").arg(url.host()).arg(url.port());
Connection *conn = new Connection(url, Connection::BearerTypeWifi, sslEnabled, displayName); Connection *conn = new Connection(url, Connection::BearerTypeWifi, sslEnabled, displayName);
conn->setOnline(true);
device->connections()->addConnection(conn); device->connections()->addConnection(conn);
} }
} }

View File

@ -12,12 +12,14 @@ ZeroconfDiscovery::ZeroconfDiscovery(DiscoveryModel *discoveryModel, QObject *pa
m_zeroconfJsonRPC = new QZeroConf(this); m_zeroconfJsonRPC = new QZeroConf(this);
connect(m_zeroconfJsonRPC, &QZeroConf::serviceAdded, this, &ZeroconfDiscovery::serviceEntryAdded); connect(m_zeroconfJsonRPC, &QZeroConf::serviceAdded, this, &ZeroconfDiscovery::serviceEntryAdded);
connect(m_zeroconfJsonRPC, &QZeroConf::serviceUpdated, 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); m_zeroconfJsonRPC->startBrowser("_jsonrpc._tcp", QAbstractSocket::AnyIPProtocol);
qDebug() << "ZeroConf: Created service browser for _jsonrpc._tcp:" << m_zeroconfJsonRPC->browserExists(); qDebug() << "ZeroConf: Created service browser for _jsonrpc._tcp:" << m_zeroconfJsonRPC->browserExists();
m_zeroconfWebSocket = new QZeroConf(this); m_zeroconfWebSocket = new QZeroConf(this);
connect(m_zeroconfWebSocket, &QZeroConf::serviceAdded, this, &ZeroconfDiscovery::serviceEntryAdded); connect(m_zeroconfWebSocket, &QZeroConf::serviceAdded, this, &ZeroconfDiscovery::serviceEntryAdded);
connect(m_zeroconfWebSocket, &QZeroConf::serviceUpdated, 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); m_zeroconfWebSocket->startBrowser("_ws._tcp", QAbstractSocket::AnyIPProtocol);
qDebug() << "TeroConf: Created service browser for _ws._tcp:" << m_zeroconfWebSocket->browserExists(); qDebug() << "TeroConf: Created service browser for _ws._tcp:" << m_zeroconfWebSocket->browserExists();
#else #else
@ -84,14 +86,72 @@ void ZeroconfDiscovery::serviceEntryAdded(const QZeroConfService &entry)
} else { } else {
url.setScheme(sslEnabled ? "wss" : "ws"); url.setScheme(sslEnabled ? "wss" : "ws");
} }
// entry
url.setHost(!entry.ip().isNull() ? entry.ip().toString() : entry.ipv6().toString()); url.setHost(!entry.ip().isNull() ? entry.ip().toString() : entry.ipv6().toString());
url.setPort(entry.port()); url.setPort(entry.port());
if (!device->connections()->find(url)){ if (!device->connections()->find(url)){
qDebug() << "Zeroconf: Adding new connection to host:" << device->name() << url.toString(); qDebug() << "Zeroconf: Adding new connection to host:" << device->name() << url.toString();
QString displayName = QString("%1:%2").arg(url.host()).arg(url.port()); QString displayName = QString("%1:%2").arg(url.host()).arg(url.port());
Connection *connection = new Connection(url, Connection::BearerTypeWifi, sslEnabled, displayName); Connection *connection = new Connection(url, Connection::BearerTypeWifi, sslEnabled, displayName);
connection->setOnline(true);
device->connections()->addConnection(connection); 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 #endif

View File

@ -28,6 +28,7 @@ private:
private slots: private slots:
void serviceEntryAdded(const QZeroConfService &entry); void serviceEntryAdded(const QZeroConfService &entry);
void serviceEntryRemoved(const QZeroConfService &entry);
#endif #endif
}; };

View File

@ -236,6 +236,7 @@ int JsonRpcClient::requestPushButtonAuth(const QString &deviceName)
void JsonRpcClient::setupRemoteAccess(const QString &idToken, const QString &userId) void JsonRpcClient::setupRemoteAccess(const QString &idToken, const QString &userId)
{ {
qDebug() << "Calling SetupRemoteAccess";
QVariantMap params; QVariantMap params;
params.insert("idToken", idToken); params.insert("idToken", idToken);
params.insert("userId", userId); params.insert("userId", userId);

View File

@ -92,3 +92,6 @@ BR=$$BRANDING
target.path = /usr/bin target.path = /usr/bin
INSTALLS += target INSTALLS += target
DISTFILES += \
ui/components/BusyOverlay.qml

View File

@ -155,12 +155,10 @@
<file>ui/images/network-vpn.svg</file> <file>ui/images/network-vpn.svg</file>
<file>translations/nymea-app-de_DE.qm</file> <file>translations/nymea-app-de_DE.qm</file>
<file>translations/nymea-app-en_US.qm</file> <file>translations/nymea-app-en_US.qm</file>
<file>ui/AppSettingsPage.qml</file>
<file>ui/images/stock_application.svg</file> <file>ui/images/stock_application.svg</file>
<file>ui/delegates/ThingDelegate.qml</file> <file>ui/delegates/ThingDelegate.qml</file>
<file>ui/images/network-secure.svg</file> <file>ui/images/network-secure.svg</file>
<file>ui/images/lock-broken.svg</file> <file>ui/images/lock-broken.svg</file>
<file>ui/AboutPage.qml</file>
<file>ui/images/sort-listitem.svg</file> <file>ui/images/sort-listitem.svg</file>
<file>ui/devicepages/ShutterDevicePage.qml</file> <file>ui/devicepages/ShutterDevicePage.qml</file>
<file>ui/images/shutter/shutter-000.svg</file> <file>ui/images/shutter/shutter-000.svg</file>
@ -242,8 +240,13 @@
<file>ui/KeyboardLoader.qml</file> <file>ui/KeyboardLoader.qml</file>
<file>ui/images/cloud.svg</file> <file>ui/images/cloud.svg</file>
<file>ui/system/CloudSettingsPage.qml</file> <file>ui/system/CloudSettingsPage.qml</file>
<file>ui/connection/CloudLoginPage.qml</file>
<file>ui/images/cloud-offline.svg</file> <file>ui/images/cloud-offline.svg</file>
<file>ui/images/cloud-error.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> </qresource>
</RCC> </RCC>

View File

@ -17,7 +17,7 @@ Page {
model: ListModel { model: ListModel {
ListElement { iconSource: "../images/share.svg"; text: qsTr("Configure things"); page: "EditDevicesPage.qml" } 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/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" } ListElement { iconSource: "../images/settings.svg"; text: qsTr("System settings"); page: "SettingsPage.qml" }
} }

View File

@ -39,6 +39,14 @@ ApplicationWindow {
property string graphStyle: "bars" property string graphStyle: "bars"
property string style: "light" property string style: "light"
property int currentMainViewIndex: 0 property int currentMainViewIndex: 0
property bool showHiddenOptions: false
property int cloudEnvironment: 0
}
Binding {
target: Engine.awsClient
property: "config"
value: settings.cloudEnvironment
} }
Component.onCompleted: { Component.onCompleted: {

View File

@ -3,7 +3,7 @@ import QtQuick.Controls 2.1
import QtQuick.Controls.Material 2.1 import QtQuick.Controls.Material 2.1
import QtQuick.Layouts 1.1 import QtQuick.Layouts 1.1
import Nymea 1.0 import Nymea 1.0
import "components" import "../components"
Page { Page {
id: root id: root
@ -27,10 +27,29 @@ Page {
spacing: app.margins spacing: app.margins
Image { Image {
id: logo
Layout.preferredHeight: app.iconSize * 2 Layout.preferredHeight: app.iconSize * 2
Layout.preferredWidth: height Layout.preferredWidth: height
fillMode: Image.PreserveAspectFit fillMode: Image.PreserveAspectFit
source: "qrc:/styles/%1/logo.svg".arg(styleController.currentStyle) 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 { GridLayout {
@ -83,60 +102,25 @@ Page {
ColumnLayout { ColumnLayout {
Layout.fillWidth: true Layout.fillWidth: true
ItemDelegate { MeaListItemDelegate {
Layout.fillWidth: true Layout.fillWidth: true
text: qsTr("Visit the nymea website")
contentItem: RowLayout {
Label {
Layout.fillWidth: true
text: qsTr("Visit the nymea website")
}
Image {
source: "images/next.svg"
Layout.preferredHeight: parent.height
Layout.preferredWidth: height
}
}
onClicked: { onClicked: {
Qt.openUrlExternally("https://nymea.io") Qt.openUrlExternally("https://nymea.io")
} }
} }
ItemDelegate { MeaListItemDelegate {
Layout.fillWidth: true Layout.fillWidth: true
text: qsTr("Visit GitHub page")
contentItem: RowLayout {
Label {
Layout.fillWidth: true
text: qsTr("Visit GitHub page")
}
Image {
source: "images/next.svg"
Layout.preferredHeight: parent.height
Layout.preferredWidth: height
}
}
onClicked: { onClicked: {
Qt.openUrlExternally("https://github.com/guh/nymea-app") Qt.openUrlExternally("https://github.com/guh/nymea-app")
} }
} }
ItemDelegate { MeaListItemDelegate {
Layout.fillWidth: true Layout.fillWidth: true
text: qsTr("View license text")
contentItem: RowLayout {
Label {
Layout.fillWidth: true
text: qsTr("View license text")
}
Image {
source: "images/next.svg"
Layout.preferredHeight: parent.height
Layout.preferredWidth: height
}
}
onClicked: { onClicked: {
pageStack.push(licenseTextComponent) pageStack.push(licenseTextComponent)
} }
@ -155,7 +139,7 @@ Page {
Layout.preferredHeight: app.iconSize * 2 Layout.preferredHeight: app.iconSize * 2
Layout.preferredWidth: height Layout.preferredWidth: height
fillMode: Image.PreserveAspectFit 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.width: app.iconSize * 2
sourceSize.height: app.iconSize * 2 sourceSize.height: app.iconSize * 2
} }
@ -166,21 +150,9 @@ Page {
wrapMode: Text.WordWrap wrapMode: Text.WordWrap
} }
} }
ItemDelegate { MeaListItemDelegate {
Layout.fillWidth: true Layout.fillWidth: true
text: qsTr("Visit the Qt website")
contentItem: RowLayout {
Label {
Layout.fillWidth: true
text: qsTr("Visit the Qt website")
}
Image {
source: "images/next.svg"
Layout.preferredHeight: parent.height
Layout.preferredWidth: height
}
}
onClicked: { onClicked: {
Qt.openUrlExternally("https://www.qt.io") Qt.openUrlExternally("https://www.qt.io")
} }

View File

@ -3,7 +3,7 @@ import QtQuick.Controls 2.1
import QtQuick.Controls.Material 2.1 import QtQuick.Controls.Material 2.1
import QtQuick.Layouts 1.1 import QtQuick.Layouts 1.1
import Nymea 1.0 import Nymea 1.0
import "components" import "../components"
Page { Page {
id: root id: root
@ -114,7 +114,7 @@ Page {
Layout.fillWidth: true Layout.fillWidth: true
text: qsTr("Cloud login") text: qsTr("Cloud login")
iconName: "../images/cloud.svg" iconName: "../images/cloud.svg"
onClicked: pageStack.push(Qt.resolvedUrl("connection/CloudLoginPage.qml")) onClicked: pageStack.push(Qt.resolvedUrl("CloudLoginPage.qml"))
} }
MeaListItemDelegate { MeaListItemDelegate {
Layout.fillWidth: true Layout.fillWidth: true
@ -122,6 +122,13 @@ Page {
iconName: "../images/info.svg" iconName: "../images/info.svg"
onClicked: pageStack.push(Qt.resolvedUrl("AboutPage.qml")) 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"))
}
} }

View 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
}
}
}
}
}

View 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;
}
}
}
}
}

View 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
}
}

View 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
}
}

View File

@ -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);
}
}
}
}

View File

@ -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 { NymeaDiscovery {
id: discovery id: discovery
objectName: "discovery" objectName: "discovery"
@ -87,15 +95,11 @@ Page {
ListElement { iconSource: "../images/network-vpn.svg"; text: qsTr("Manual connection"); page: "ManualConnectPage.qml" } 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/bluetooth.svg"; text: qsTr("Wireless setup"); page: "BluetoothDiscoveryPage.qml"; }
ListElement { iconSource: "../images/private-browsing.svg"; text: qsTr("Demo mode"); page: "" } 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: { onClicked: {
if (index === 2) { if (index === 2) {
Engine.connection.connect("nymea://nymea.nymea.io:2222") root.connectToHost("nymea://nymea.nymea.io:2222")
var page = pageStack.push(Qt.resolvedUrl("ConnectingPage.qml"))
page.cancel.connect(function() {
Engine.connection.disconnect()
})
} else { } else {
pageStack.push(model.get(index).page); pageStack.push(model.get(index).page);
} }
@ -162,11 +166,12 @@ Page {
var bearerPreference = [Connection.BearerTypeEthernet, Connection.BearerTypeWifi, Connection.BearerTypeBluetooth, Connection.BearerTypeCloud] var bearerPreference = [Connection.BearerTypeEthernet, Connection.BearerTypeWifi, Connection.BearerTypeBluetooth, Connection.BearerTypeCloud]
var oldBearerPriority = bearerPreference.indexOf(oldConfig.bearerType); var oldBearerPriority = bearerPreference.indexOf(oldConfig.bearerType);
var newBearerPriority = bearerPreference.indexOf(newConfig.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; usedConfigIndex = i;
continue; continue;
} }
if (oldBearerPriority > newBearerPriority) { if (oldBearerPriority < newBearerPriority) {
continue; // discard new one the one we have is on a better bearer type continue; // discard new one the one we have is on a better bearer type
} }
@ -208,16 +213,15 @@ Page {
progressive: false progressive: false
property bool isSecure: discoveryDevice.connections.get(defaultConnectionIndex).secure property bool isSecure: discoveryDevice.connections.get(defaultConnectionIndex).secure
property bool isTrusted: Engine.connection.isTrusted(discoveryDeviceDelegate.discoveryDevice.connections.get(defaultConnectionIndex).url) property bool isTrusted: Engine.connection.isTrusted(discoveryDeviceDelegate.discoveryDevice.connections.get(defaultConnectionIndex).url)
secondaryIconName: isSecure ? "../images/network-secure.svg" : "" property bool isOnline: discoveryDevice.connections.get(defaultConnectionIndex).online
secondaryIconColor: isTrusted ? app.accentColor : Material.foreground 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 swipe.enabled: discoveryDeviceDelegate.discoveryDevice.deviceType === DiscoveryDevice.DeviceTypeNetwork
onClicked: { onClicked: {
Engine.connection.connect(discoveryDeviceDelegate.discoveryDevice.connections.get(defaultConnectionIndex).url) root.connectToHost(discoveryDeviceDelegate.discoveryDevice.connections.get(defaultConnectionIndex).url)
var page = pageStack.push(Qt.resolvedUrl("ConnectingPage.qml"))
page.cancel.connect(function() {
Engine.connection.disconnect()
})
} }
swipe.right: MouseArea { swipe.right: MouseArea {
@ -279,7 +283,7 @@ Page {
Layout.rightMargin: app.margins Layout.rightMargin: app.margins
text: qsTr("Cloud login") text: qsTr("Cloud login")
visible: !Engine.awsClient.isLoggedIn visible: !Engine.awsClient.isLoggedIn
onClicked: pageStack.push(Qt.resolvedUrl("CloudLoginPage.qml")) onClicked: pageStack.push(Qt.resolvedUrl("../appsettings/CloudLoginPage.qml"))
} }
Button { Button {
@ -287,14 +291,10 @@ Page {
Layout.leftMargin: app.margins Layout.leftMargin: app.margins
Layout.rightMargin: app.margins Layout.rightMargin: app.margins
Layout.bottomMargin: app.margins Layout.bottomMargin: app.margins
// visible: discovery.discoveryModel.count === 0 visible: discovery.discoveryModel.count === 0
text: qsTr("Demo mode (online)") text: qsTr("Demo mode (online)")
onClicked: { onClicked: {
Engine.connection.connect("nymea://nymea.nymea.io:2222") root.connectToHost("nymea://nymea.nymea.io:2222")
var page = pageStack.push(Qt.resolvedUrl("ConnectingPage.qml"))
page.cancel.connect(function() {
Engine.connection.disconnect()
})
} }
} }
@ -420,7 +420,7 @@ Page {
onAccepted: { onAccepted: {
Engine.connection.acceptCertificate(certDialog.url, certDialog.fingerprint) Engine.connection.acceptCertificate(certDialog.url, certDialog.fingerprint)
Engine.connection.connect(certDialog.url) root.connectToHost(certDialog.url)
} }
} }
} }
@ -532,12 +532,14 @@ Page {
return "" return ""
} }
secondaryIconName: model.secure ? "../images/network-secure.svg" : "" tertiaryIconName: model.secure ? "../images/network-secure.svg" : ""
secondaryIconColor: isTrusted ? app.accentColor : "gray" tertiaryIconColor: isTrusted ? app.accentColor : "gray"
readonly property bool isTrusted: Engine.connection.isTrusted(url) readonly property bool isTrusted: Engine.connection.isTrusted(url)
secondaryIconName: !model.online ? "../images/cloud-error.svg" : ""
secondaryIconColor: "red"
onClicked: { onClicked: {
Engine.connection.connect(dialog.discoveryDevice.connections.get(index).url) root.connectToHost(dialog.discoveryDevice.connections.get(index).url)
dialog.close() dialog.close()
} }
} }

View File

@ -26,6 +26,13 @@ Page {
wrapMode: Text.WordWrap wrapMode: Text.WordWrap
horizontalAlignment: Text.AlignHCenter horizontalAlignment: Text.AlignHCenter
} }
Label {
Layout.fillWidth: true
text: Engine.connection.url
font.pixelSize: app.smallFont
wrapMode: Text.WrapAtWordBoundaryOrAnywhere
horizontalAlignment: Text.AlignHCenter
}
} }
Button { Button {

View File

@ -9,15 +9,15 @@
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="96" width="90"
height="96" height="90"
id="svg4874" id="svg6138"
version="1.1" version="1.1"
inkscape:version="0.91+devel r" inkscape:version="0.91+devel r"
viewBox="0 0 96 96.000001" viewBox="0 0 90 90.000001"
sodipodi:docname="weather-clouds-symbolic.svg"> sodipodi:docname="sync-idle.svg">
<defs <defs
id="defs4876" /> id="defs6140" />
<sodipodi:namedview <sodipodi:namedview
id="base" id="base"
pagecolor="#ffffff" pagecolor="#ffffff"
@ -25,91 +25,87 @@
borderopacity="1.0" borderopacity="1.0"
inkscape:pageopacity="0.0" inkscape:pageopacity="0.0"
inkscape:pageshadow="2" inkscape:pageshadow="2"
inkscape:zoom="4.4959994" inkscape:zoom="6.3664629"
inkscape:cx="11.154354" inkscape:cx="-7.1609628"
inkscape:cy="34.719727" inkscape:cy="35.214212"
inkscape:document-units="px" inkscape:document-units="px"
inkscape:current-layer="g4780" inkscape:current-layer="g6253"
showgrid="false" showgrid="true"
showborder="true"
fit-margin-top="0" fit-margin-top="0"
fit-margin-left="0" fit-margin-left="0"
fit-margin-right="0" fit-margin-right="0"
fit-margin-bottom="0" fit-margin-bottom="0"
inkscape:snap-global="true"
inkscape:snap-bbox="true" inkscape:snap-bbox="true"
inkscape:bbox-paths="true" inkscape:bbox-paths="true"
inkscape:bbox-nodes="true"
inkscape:snap-bbox-edge-midpoints="true"
inkscape:snap-bbox-midpoints="true" inkscape:snap-bbox-midpoints="true"
inkscape:snap-bbox-edge-midpoints="true"
inkscape:bbox-nodes="true"
inkscape:object-paths="true" inkscape:object-paths="true"
inkscape:snap-intersection-paths="true" inkscape:snap-intersection-paths="true"
inkscape:object-nodes="true" inkscape:object-nodes="true"
inkscape:snap-smooth-nodes="true" inkscape:snap-smooth-nodes="true"
inkscape:snap-midpoints="true" inkscape:snap-midpoints="true"
inkscape:snap-others="true"
inkscape:snap-object-midpoints="true" inkscape:snap-object-midpoints="true"
inkscape:snap-center="true" inkscape:snap-center="true"
showguides="true" showguides="true"
inkscape:guide-bbox="true" inkscape:guide-bbox="true">
inkscape:snap-global="true">
<inkscape:grid <inkscape:grid
type="xygrid" type="xygrid"
id="grid5451" id="grid6700"
empspacing="8" /> empspacing="6" />
<sodipodi:guide
orientation="1,0"
position="8,-8.0000001"
id="guide4063" />
<sodipodi:guide
orientation="1,0"
position="4,-8.0000001"
id="guide4065" />
<sodipodi:guide <sodipodi:guide
orientation="0,1" orientation="0,1"
position="-8,88.000001" position="62,87"
id="guide4067" /> id="guide4084" />
<sodipodi:guide <sodipodi:guide
orientation="0,1" orientation="0,1"
position="-8,92.000001" position="63,84"
id="guide4069" /> id="guide4086" />
<sodipodi:guide <sodipodi:guide
orientation="0,1" orientation="0,1"
position="104,4" position="63,81"
id="guide4071" /> 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 <sodipodi:guide
orientation="0,1" orientation="0,1"
position="-5,8.0000001" position="60,3"
id="guide4073" /> id="guide4102" />
<sodipodi:guide
orientation="1,0"
position="88,-8.0000001"
id="guide4077" />
<sodipodi:guide <sodipodi:guide
orientation="0,1" orientation="0,1"
position="-8,84.000001" position="61,6"
id="guide4074" /> id="guide4104" />
<sodipodi:guide <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" orientation="0,1"
id="guide4172" /> position="62,9"
<sodipodi:guide id="guide4106" />
position="92,-8.0000001"
orientation="1,0"
id="guide4760" />
</sodipodi:namedview> </sodipodi:namedview>
<metadata <metadata
id="metadata4879"> id="metadata6143">
<rdf:RDF> <rdf:RDF>
<cc:Work <cc:Work
rdf:about=""> rdf:about="">
@ -124,37 +120,25 @@
inkscape:label="Layer 1" inkscape:label="Layer 1"
inkscape:groupmode="layer" inkscape:groupmode="layer"
id="layer1" id="layer1"
transform="translate(67.857146,-78.50504)"> transform="translate(-283.57144,-358.79068)">
<g <g
transform="matrix(0,-1,-1,0,373.50506,516.50504)" id="g6253"
id="g4845" inkscape:export-filename="planemode01.png"
style="display:inline"> inkscape:export-xdpi="90"
<g inkscape:export-ydpi="90"
inkscape:export-ydpi="90" transform="matrix(-1,0,0,1,547.57143,-1341.5715)">
inkscape:export-xdpi="90" <rect
inkscape:export-filename="next01.png" style="fill:none;stroke:none"
transform="matrix(-0.9996045,0,0,1,575.94296,-611.00001)" id="rect6257"
id="g4778" width="90"
inkscape:label="Layer 1"> height="90"
<g x="174"
transform="matrix(-1,0,0,1,575.99999,611)" y="1700.3622" />
id="g4780" <path
style="display:inline"> 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"
<rect 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"
style="color:#000000;display:inline;overflow:visible;visibility:visible;fill:none;stroke:none;stroke-width:4;marker:none;enable-background:accumulate" id="path4154"
id="rect4782" inkscape:connector-curvature="0" />
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>
</g> </g>
</g> </g>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 6.3 KiB

After

Width:  |  Height:  |  Size: 6.0 KiB

View File

@ -11,21 +11,16 @@ Page {
onBackPressed: pageStack.pop(); onBackPressed: pageStack.pop();
} }
Connections { Item {
target: Engine.basicConfiguration id: d
onCloudEnabledChanged: { property bool deploymentStarted: false
if (Engine.jsonRpcClient.cloudConnectionState === JsonRpcClient.CloudConnectionStateUnconfigured) {
Engine.deployCertificate();
}
}
}
Connections { Connections {
target: Engine.jsonRpcClient target: Engine.jsonRpcClient
onCloudConnectionStateChanged: { onCloudConnectionStateChanged: {
if (Engine.awsClient.isLoggedIn && Engine.awsClient.awsDevices.getDevice(Engine.jsonRpcClient.serverUuid) === null) { if (Engine.jsonRpcClient.cloudConnectionState == JsonRpcClient.CloudConnectionStateConnected) {
print("Pairing user and box...") d.deploymentStarted = false;
Engine.jsonRpcClient.setupRemoteAccess(Engine.awsClient.idToken, Engine.awsClient.userId); }
} }
} }
} }
@ -51,33 +46,67 @@ Page {
} }
} }
ThinDivider {}
RowLayout { RowLayout {
Layout.fillWidth: true Layout.fillWidth: true
Layout.leftMargin: app.margins Layout.leftMargin: app.margins
Layout.rightMargin: app.margins Layout.rightMargin: app.margins
visible: Engine.basicConfiguration.cloudEnabled
BusyIndicator { ColorIcon {
visible: Engine.jsonRpcClient.cloudConnectionState == JsonRpcClient.CloudConnectionStateUnconfigured || Layout.preferredHeight: busyIndicator.height
Engine.jsonRpcClient.cloudConnectionState == JsonRpcClient.CloudConnectionStateConnecting 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 { Label {
Layout.fillWidth: true Layout.fillWidth: true
wrapMode: Text.WordWrap wrapMode: Text.WordWrap
text: { text: {
switch (Engine.jsonRpcClient.cloudConnectionState) { switch (Engine.jsonRpcClient.cloudConnectionState) {
case JsonRpcClient.CloudConnectionStateDisabled: case JsonRpcClient.CloudConnectionStateDisabled:
return "" return qsTr("This box is not connected to %1:cloud").arg(app.systemName)
case JsonRpcClient.CloudConnectionStateUnconfigured: 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: 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: 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 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