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 "sigv4utils.h"
static QByteArray clientId = "8rjhfdlf9jf1suok2jcrltd6v";
static QByteArray region = "eu-west-1";
//static QByteArray service = "iotdevicegateway";
static QByteArray service = "iotdata";
// This is Symantec's root CA certificate and most platforms should
// have this in their certificate storage already, but as we can't
// be certain about the core's setup, let's deploy it ourselves.
static QByteArray rootCA = "-----BEGIN CERTIFICATE-----\n\
MIIE0zCCA7ugAwIBAgIQGNrRniZ96LtKIVjNzGs7SjANBgkqhkiG9w0BAQUFADCB\n\
yjELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDlZlcmlTaWduLCBJbmMuMR8wHQYDVQQL\n\
@ -51,6 +50,31 @@ AWSClient::AWSClient(QObject *parent) : QObject(parent),
{
m_nam = new QNetworkAccessManager(this);
AWSConfiguration config;
// Community environment
config.clientId = "35duli0b13c7pet5k4bcv8pbu";
config.poolId = "eu-west-1_WZVsaBsaY";
config.identityPoolId = "eu-west-1:17449947-1a2f-4dda-aa49-7c5b1eec78d7";
config.certificateEndpoint = "https://communityservice-cloud.nymea.io/certificatews/certificate";
config.certificateApiKey = "aIRQv4yDdF6ASq12X1CPp7b6MpkdODfI3AOjOnkE";
config.certificateVendorId = "d399290a-0599-4895-b4c3-34d2bdb579f4";
config.mqttEndpoint = "a2d0ba9572wepp.iot.eu-west-1.amazonaws.com";
config.region = "eu-west-1";
config.apiEndpoint = "api-cloud.guh.io";
m_configs.append(config);
// Testing environment
config.clientId = "8rjhfdlf9jf1suok2jcrltd6v";
config.poolId = "eu-west-1_6eX6YjmXr";
config.identityPoolId = "eu-west-1:108a174c-5786-40f9-966a-1a0cd33d6801";
config.certificateEndpoint = "https://testcommunityservice-cloud.nymea.io/certificatews/certificate";
config.certificateApiKey = "VhmAUy75eZ9jXaUEjgWZh9PpSIykPGBK7AZFPimh";
config.certificateVendorId = "testVendor001";
config.mqttEndpoint = "a2addxakg5juii.iot.eu-west-1.amazonaws.com";
config.region = "eu-west-1";
config.apiEndpoint = "testapi-cloud.guh.io";
m_configs.append(config);
QSettings settings;
settings.beginGroup("cloud");
m_username = settings.value("username").toString();
@ -59,6 +83,7 @@ AWSClient::AWSClient(QObject *parent) : QObject(parent),
m_accessTokenExpiry = settings.value("accessTokenExpiry").toDateTime();
m_idToken = settings.value("idToken").toByteArray();
m_refreshToken = settings.value("refreshToken").toByteArray();
m_confirmationPending = settings.value("confirmationPending", false).toBool();
m_identityId = settings.value("identityId").toByteArray();
@ -88,16 +113,22 @@ AWSDevices *AWSClient::awsDevices() const
return m_devices;
}
bool AWSClient::confirmationPending() const
{
return m_confirmationPending;
}
void AWSClient::login(const QString &username, const QString &password)
{
m_username = username;
// Ok... Please fogive me for this... AWS APIs are just unbearable... can't be bothered
// any more to walk through another chain of calls in order to have the refreshToken working.
// Due to an issue in AWS apis it's very complex to use the refresh token. Taking a shortcut here for now:
// Will store the password in the config for now and re-login when the accessToken expires.
// See: https://forums.aws.amazon.com/thread.jspa?threadID=287978
// Ideally we'd use the refresh token and not store the password at all (see: refreshAccessToken())
m_password = password;
QUrl url("https://cognito-idp.eu-west-1.amazonaws.com/");
QString host = QString("cognito-idp.%1.amazonaws.com").arg(m_configs.at(m_usedConfigIndex).region);
QUrl url(QString("https://%1/").arg(host));
QUrlQuery query;
query.addQueryItem("Action", "InitiateAuth");
@ -106,12 +137,12 @@ void AWSClient::login(const QString &username, const QString &password)
QNetworkRequest request(url);
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-amz-json-1.0");
request.setRawHeader("Host", "cognito-idp.eu-west-1.amazonaws.com");
request.setRawHeader("Host", host.toUtf8());
request.setRawHeader("X-Amz-Target", "AWSCognitoIdentityProviderService.InitiateAuth");
QVariantMap params;
params.insert("AuthFlow", "USER_PASSWORD_AUTH");
params.insert("ClientId", clientId);
params.insert("ClientId", m_configs.at(m_usedConfigIndex).clientId);
QVariantMap authParams;
authParams.insert("USERNAME", username);
@ -131,6 +162,7 @@ void AWSClient::login(const QString &username, const QString &password)
qWarning() << "Error logging in to aws:" << reply->error() << reply->errorString();
m_username.clear();
m_password.clear();
emit loginResult(LoginErrorUnknownError);
return;
}
QByteArray data = reply->readAll();
@ -140,6 +172,7 @@ void AWSClient::login(const QString &username, const QString &password)
qWarning() << "Failed to parse AWS login response" << error.errorString();
m_username.clear();
m_password.clear();
emit loginResult(LoginErrorUnknownError);
return;
}
@ -160,7 +193,7 @@ void AWSClient::login(const QString &username, const QString &password)
settings.setValue("idToken", m_idToken);
settings.setValue("refreshToken", m_refreshToken);
qDebug() << "AWS login successful" << qUtf8Printable(jsonDoc.toJson(QJsonDocument::Indented));
qDebug() << "AWS login successful";// << qUtf8Printable(jsonDoc.toJson(QJsonDocument::Indented));
emit isLoggedInChanged();
qDebug() << "Getting cognito ID";
@ -177,9 +210,310 @@ void AWSClient::logout()
emit isLoggedInChanged();
}
void AWSClient::signup(const QString &username, const QString &password)
{
m_userId = QUuid::createUuid().toString().remove(QRegExp("[{}]"));
m_username = username;
m_password = password;
QString host = QString("cognito-idp.%1.amazonaws.com").arg(m_configs.at(m_usedConfigIndex).region);
QUrl url(QString("https://%1/").arg(host));
QUrlQuery query;
query.addQueryItem("Action", "SignUp");
query.addQueryItem("Version", "2016-04-18");
url.setQuery(query);
QNetworkRequest request(url);
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-amz-json-1.0");
request.setRawHeader("Host", host.toUtf8());
request.setRawHeader("X-Amz-Target", "AWSCognitoIdentityProviderService.SignUp");
QVariantMap params;
params.insert("ClientId", m_configs.at(m_usedConfigIndex).clientId);
params.insert("Username", m_userId);
params.insert("Password", password);
QVariantMap emailAttribute;
emailAttribute.insert("Name", "email");
emailAttribute.insert("Value", username);
QVariantList userAttributes;
userAttributes.append(emailAttribute);
params.insert("UserAttributes", userAttributes);
QJsonDocument jsonDoc = QJsonDocument::fromVariant(params);
QByteArray payload = jsonDoc.toJson(QJsonDocument::Compact);
qDebug() << "Signing up to AWS as user:" << username << payload;
QNetworkReply *reply = m_nam->post(request, payload);
connect(reply, &QNetworkReply::finished, this, [this, reply]() {
QByteArray data = reply->readAll();
reply->deleteLater();
qDebug() << "AWS signup reply:" << data;
if (reply->error() == QNetworkReply::ProtocolInvalidOperationError) {
emit signupResult(LoginErrorInvalidUserOrPass);
return;
}
if (reply->error() != QNetworkReply::NoError) {
qWarning() << "Error signing up to aws:" << reply->error() << reply->errorString();
m_username.clear();
m_password.clear();
emit signupResult(LoginErrorUnknownError);
return;
}
emit signupResult(LoginErrorNoError);
QSettings settings;
settings.beginGroup("cloud");
settings.setValue("username", m_username);
settings.setValue("password", m_password);
settings.setValue("confirmationPending", true);
m_confirmationPending = true;
emit confirmationPendingChanged();
});
}
void AWSClient::confirmRegistration(const QString &code)
{
QString host = QString("cognito-idp.%1.amazonaws.com").arg(m_configs.at(m_usedConfigIndex).region);
QUrl url(QString("https://%1/").arg(host));
QUrlQuery query;
query.addQueryItem("Action", "ConfirmSignUp");
query.addQueryItem("Version", "2016-04-18");
url.setQuery(query);
QNetworkRequest request(url);
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-amz-json-1.0");
request.setRawHeader("Host", host.toUtf8());
request.setRawHeader("X-Amz-Target", "AWSCognitoIdentityProviderService.ConfirmSignUp");
QVariantMap params;
params.insert("ClientId", m_configs.at(m_usedConfigIndex).clientId);
params.insert("Username", m_userId);
params.insert("ConfirmationCode", code);
QJsonDocument jsonDoc = QJsonDocument::fromVariant(params);
QByteArray payload = jsonDoc.toJson(QJsonDocument::Compact);
qDebug() << "Confirming registration for user:" << m_username;
QNetworkReply *reply = m_nam->post(request, payload);
connect(reply, &QNetworkReply::finished, this, [this, reply]() {
QByteArray data = reply->readAll();
reply->deleteLater();
qDebug() << "AWS signup reply:" << data;
if (reply->error() == QNetworkReply::ProtocolInvalidOperationError) {
QJsonParseError error;
QVariantMap result = QJsonDocument::fromJson(data, &error).toVariant().toMap();
if (result.value("__type").toString() == "com.amazonaws.cognito.identity.idp.model#CodeMismatchException") {
emit confirmationResult(LoginErrorInvalidCode);
return;
} else if (result.value("__type").toString() == "com.amazonaws.cognito.identity.idp.model#AliasExistsException") {
emit confirmationResult(LoginErrorUserExists);
return;
}
emit confirmationResult(LoginErrorUnknownError);
return;
}
if (reply->error() != QNetworkReply::NoError) {
qWarning() << "Error confirming registration:" << reply->error() << reply->errorString();
emit confirmationResult(LoginErrorUnknownError);
return;
}
emit confirmationResult(LoginErrorNoError);
login(m_username, m_password);
fetchDevices();
});
}
void AWSClient::forgotPassword(const QString &username)
{
QString host = QString("cognito-idp.%1.amazonaws.com").arg(m_configs.at(m_usedConfigIndex).region);
QUrl url(QString("https://%1/").arg(host));
QUrlQuery query;
query.addQueryItem("Action", "ForgotPassword");
query.addQueryItem("Version", "2016-04-18");
url.setQuery(query);
QNetworkRequest request(url);
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-amz-json-1.0");
request.setRawHeader("Host", host.toUtf8());
request.setRawHeader("X-Amz-Target", "AWSCognitoIdentityProviderService.ForgotPassword");
QVariantMap params;
params.insert("ClientId", m_configs.at(m_usedConfigIndex).clientId);
params.insert("Username", username);
QJsonDocument jsonDoc = QJsonDocument::fromVariant(params);
QByteArray payload = jsonDoc.toJson(QJsonDocument::Compact);
qDebug() << "Forgot password for user:" << username << payload;
QNetworkReply *reply = m_nam->post(request, payload);
connect(reply, &QNetworkReply::finished, this, [this, reply]() {
QByteArray data = reply->readAll();
reply->deleteLater();
if (reply->error() == QNetworkReply::ProtocolInvalidOperationError) {
QJsonDocument jsonDoc = QJsonDocument::fromJson(data);
if (jsonDoc.toVariant().toMap().value("__type").toString() == "com.amazonaws.cognito.identity.idp.model#LimitExceededException") {
emit forgotPasswordResult(LoginErrorLimitExceeded);
return;
}
}
if (reply->error() != QNetworkReply::NoError) {
qWarning() << "Error calling ForgotPassword:" << reply->error() << reply->errorString() << data;
emit forgotPasswordResult(LoginErrorUnknownError);
return;
}
qDebug() << "AWS forgotPassword success:" << data;
emit forgotPasswordResult(LoginErrorNoError);
});
}
void AWSClient::confirmForgotPassword(const QString &username, const QString &code, const QString &newPassword)
{
QString host = QString("cognito-idp.%1.amazonaws.com").arg(m_configs.at(m_usedConfigIndex).region);
QUrl url(QString("https://%1/").arg(host));
QUrlQuery query;
query.addQueryItem("Action", "ConfirmForgotPassword");
query.addQueryItem("Version", "2016-04-18");
url.setQuery(query);
QNetworkRequest request(url);
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-amz-json-1.0");
request.setRawHeader("Host", host.toUtf8());
request.setRawHeader("X-Amz-Target", "AWSCognitoIdentityProviderService.ConfirmForgotPassword");
QVariantMap params;
params.insert("ClientId", m_configs.at(m_usedConfigIndex).clientId);
params.insert("ConfirmationCode", code);
params.insert("Username", username);
params.insert("Password", newPassword);
QJsonDocument jsonDoc = QJsonDocument::fromVariant(params);
QByteArray payload = jsonDoc.toJson(QJsonDocument::Compact);
qDebug() << "ConfirmForgotPassword for user:" << username << payload;
QNetworkReply *reply = m_nam->post(request, payload);
connect(reply, &QNetworkReply::finished, this, [this, reply]() {
QByteArray data = reply->readAll();
reply->deleteLater();
if (reply->error() != QNetworkReply::NoError) {
qWarning() << "Error calling ConfirmForgotPassword:" << reply->error() << reply->errorString() << data;
emit confirmForgotPasswordResult(LoginErrorUnknownError);
return;
}
qDebug() << "AWS ConfirmForgotPassword success:" << data;
emit confirmForgotPasswordResult(LoginErrorNoError);
});
}
void AWSClient::deleteAccount()
{
if (!isLoggedIn()) {
qWarning() << "Not logged in at AWS. Can't delete account";
return;
}
if (tokensExpired()) {
qDebug() << "Cannot unpair device. Need to refresh our tokens";
refreshAccessToken();
m_callQueue.append(QueuedCall("deleteAccount"));
return;
}
qDebug() << "Deleting account";
QUrl url(QString("https://%1/users/profiles/%2").arg(m_configs.at(m_usedConfigIndex).apiEndpoint).arg(m_username));
QNetworkRequest request(url);
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
request.setRawHeader("x-api-idToken", m_idToken);
qDebug() << "DELETE" << url.toString();
qDebug() << "HEADERS:";
foreach (const QByteArray &hdr, request.rawHeaderList()) {
qDebug() << hdr << ":" << request.rawHeader(hdr);
}
QNetworkReply *reply = m_nam->deleteResource(request);
connect(reply, &QNetworkReply::finished, this, [this, reply]() {
reply->deleteLater();
QByteArray data = reply->readAll();
if (reply->error() != QNetworkReply::NoError) {
qWarning() << "Error deleting cloud user account:" << reply->error() << reply->errorString() << qUtf8Printable(data);
return;
}
QJsonParseError error;
QJsonDocument jsonDoc = QJsonDocument::fromJson(data, &error);
if (error.error != QJsonParseError::NoError) {
qWarning() << "Failed to parse JSON from server" << error.errorString() << qUtf8Printable(data);
return;
}
logout();
qDebug() << "Account deleted" << data;
});
}
void AWSClient::unpairDevice(const QString &boxId)
{
if (!isLoggedIn()) {
qWarning() << "Not logged in at AWS. Can't unpair device";
return;
}
if (tokensExpired()) {
qDebug() << "Cannot unpair device. Need to refresh our tokens";
refreshAccessToken();
m_callQueue.append(QueuedCall("unpairDevice", boxId));
return;
}
qDebug() << "unpairing device";
QUrl url(QString("https://%1/users/devices/%2").arg(m_configs.at(m_usedConfigIndex).apiEndpoint).arg(boxId));
QNetworkRequest request(url);
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
request.setRawHeader("x-api-idToken", m_idToken);
QNetworkReply *reply = m_nam->deleteResource(request);
connect(reply, &QNetworkReply::finished, this, [this, reply, boxId]() {
reply->deleteLater();
QByteArray data = reply->readAll();
if (reply->error() != QNetworkReply::NoError) {
qWarning() << "Error unpairing cloud device:" << reply->error() << reply->errorString() << qUtf8Printable(data);
return;
}
QJsonParseError error;
QJsonDocument jsonDoc = QJsonDocument::fromJson(data, &error);
if (error.error != QJsonParseError::NoError) {
qWarning() << "Failed to parse JSON from server" << error.errorString() << qUtf8Printable(data);
return;
}
qDebug() << "Device unpaired" << data;
m_devices->remove(boxId);
});
}
void AWSClient::getId()
{
QUrl url("https://cognito-identity.eu-west-1.amazonaws.com/");
QString host = QString("cognito-identity.%1.amazonaws.com").arg(m_configs.at(m_usedConfigIndex).region);
QUrl url(QString("https://%1/").arg(host));
QUrlQuery query;
query.addQueryItem("Action", "GetId");
@ -188,19 +522,21 @@ void AWSClient::getId()
QNetworkRequest request(url);
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-amz-json-1.0");
request.setRawHeader("Host", "cognito-identity.eu-west-1.amazonaws.com");
request.setRawHeader("Host", host.toUtf8());
request.setRawHeader("X-Amz-Target", "AWSCognitoIdentityService.GetId");
QVariantMap logins;
logins.insert("cognito-idp.eu-west-1.amazonaws.com/eu-west-1_6eX6YjmXr", m_idToken);
logins.insert(QString("cognito-idp.%1.amazonaws.com/%2").arg(m_configs.at(m_usedConfigIndex).region).arg(m_configs.at(m_usedConfigIndex).poolId).toUtf8(), m_idToken);
QVariantMap params;
params.insert("IdentityPoolId", "eu-west-1:108a174c-5786-40f9-966a-1a0cd33d6801");
params.insert("IdentityPoolId", m_configs.at(m_usedConfigIndex).identityPoolId.toUtf8());
params.insert("Logins", logins);
QJsonDocument jsonDoc = QJsonDocument::fromVariant(params);
QByteArray payload = jsonDoc.toJson(QJsonDocument::Compact);
// qDebug() << "Posting:" << request.url().toString();
// qDebug() << "Payload:" << payload;
QNetworkReply *reply = m_nam->post(request, payload);
connect(reply, &QNetworkReply::finished, this, [this, reply]() {
reply->deleteLater();
@ -220,7 +556,7 @@ void AWSClient::getId()
settings.beginGroup("cloud");
settings.setValue("identityId", m_identityId);
qDebug() << "Received cognito identity id" << m_identityId;
qDebug() << "Received cognito identity id" << m_identityId;// << qUtf8Printable(data);
getCredentialsForIdentity(m_identityId);
});
@ -240,13 +576,13 @@ void AWSClient::fetchCertificate(const QString &uuid, std::function<void(const Q
{
QString fixedUuid = uuid;
fixedUuid.remove(QRegExp("[{}]"));
QNetworkRequest request(QUrl("https://testproductionservice-cloud.guh.io/certificatews/certificate"));
request.setRawHeader("X-api-key", "BJMq4h19dB5yjVKwagTvk9u72FqLecEoWPJIkyDj");
request.setRawHeader("X-api-vendorId", "testVendor001");
QNetworkRequest request(m_configs.at(m_usedConfigIndex).certificateEndpoint);
request.setRawHeader("X-api-key", m_configs.at(m_usedConfigIndex).certificateApiKey.toUtf8());
request.setRawHeader("X-api-vendorId", m_configs.at(m_usedConfigIndex).certificateVendorId.toUtf8());
request.setRawHeader("X-api-deviceId", fixedUuid.toUtf8());
request.setRawHeader("X-api-serialId", "69696969");
QNetworkReply *reply = m_nam->get(request);
connect(reply, &QNetworkReply::finished, this, [reply, callback]() {
connect(reply, &QNetworkReply::finished, this, [this, reply, callback]() {
reply->deleteLater();
QByteArray data = reply->readAll();
if (reply->error() != QNetworkReply::NoError) {
@ -266,14 +602,29 @@ void AWSClient::fetchCertificate(const QString &uuid, std::function<void(const Q
qDebug() << "Certificate received" << certificate;
qDebug() << "Public key" << publicKey;
qDebug() << "Private key" << privateKey;
callback(rootCA, certificate, publicKey, privateKey, "a2addxakg5juii.iot.eu-west-1.amazonaws.com");
callback(rootCA, certificate, publicKey, privateKey, m_configs.at(m_usedConfigIndex).mqttEndpoint);
});
}
int AWSClient::config() const
{
return m_usedConfigIndex;
}
void AWSClient::setConfig(int index)
{
if (m_usedConfigIndex != index) {
qDebug() << "Setting AWS configuration to" << index;
m_usedConfigIndex = index;
emit configChanged();
}
}
void AWSClient::getCredentialsForIdentity(const QString &identityId)
{
QUrl url("https://cognito-identity.eu-west-1.amazonaws.com/");
QString host = QString("cognito-identity.%1.amazonaws.com").arg(m_configs.at(m_usedConfigIndex).region);
QUrl url(QString("https://%1/").arg(host));
QUrlQuery query;
query.addQueryItem("Action", "GetCredentialsForIdentity");
@ -282,11 +633,11 @@ void AWSClient::getCredentialsForIdentity(const QString &identityId)
QNetworkRequest request(url);
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-amz-json-1.0");
request.setRawHeader("Host", "cognito-identity.eu-west-1.amazonaws.com");
request.setRawHeader("Host", host.toUtf8());
request.setRawHeader("X-Amz-Target", "AWSCognitoIdentityService.GetCredentialsForIdentity");
QVariantMap logins;
logins.insert("cognito-idp.eu-west-1.amazonaws.com/eu-west-1_6eX6YjmXr", m_idToken);
logins.insert(QString("cognito-idp.eu-west-1.amazonaws.com/%1").arg(m_configs.at(m_usedConfigIndex).poolId), m_idToken);
QVariantMap params;
params.insert("IdentityId", identityId);
@ -295,18 +646,19 @@ void AWSClient::getCredentialsForIdentity(const QString &identityId)
QJsonDocument jsonDoc = QJsonDocument::fromVariant(params);
QByteArray payload = jsonDoc.toJson(QJsonDocument::Compact);
qDebug() << "Calling GetCredentialsForIdentity:" << request.url();
qDebug() << "Headers:";
foreach (const QByteArray &headerName, request.rawHeaderList()) {
qDebug() << headerName << ":" << request.rawHeader(headerName);
}
qDebug() << "Payload:" << qUtf8Printable(payload);
// qDebug() << "Calling GetCredentialsForIdentity:" << request.url();
// qDebug() << "Headers:";
// foreach (const QByteArray &headerName, request.rawHeaderList()) {
// qDebug() << headerName << ":" << request.rawHeader(headerName);
// }
// qDebug() << "Payload:" << qUtf8Printable(payload);
QNetworkReply *reply = m_nam->post(request, payload);
connect(reply, &QNetworkReply::finished, this, [this, reply]() {
reply->deleteLater();
if (reply->error() != QNetworkReply::NoError) {
qWarning() << "Error calling GetCredentialsForIdentity" << reply->errorString();
emit loginResult(LoginErrorUnknownError);
return;
}
QByteArray data = reply->readAll();
@ -314,6 +666,7 @@ void AWSClient::getCredentialsForIdentity(const QString &identityId)
QJsonDocument jsonDoc = QJsonDocument::fromJson(data, &error);
if (error.error != QJsonParseError::NoError) {
qWarning() << "Error parsing JSON reply from GetCredentialsForIdentity" << error.errorString();
emit loginResult(LoginErrorUnknownError);
return;
}
QVariantMap credentialsMap = jsonDoc.toVariant().toMap().value("Credentials").toMap();
@ -330,7 +683,8 @@ void AWSClient::getCredentialsForIdentity(const QString &identityId)
settings.setValue("sessionToken", m_sessionToken);
settings.setValue("sessionTokenExpiry", m_sessionTokenExpiry);
qDebug() << "AWS Credentials for Identity received.";
qDebug() << "AWS Credentials for Identity received.";// << data;
emit loginResult(LoginErrorNoError);
while (!m_callQueue.isEmpty()) {
QueuedCall qc = m_callQueue.takeFirst();
@ -360,7 +714,6 @@ bool AWSClient::postToMQTT(const QString &boxId, std::function<void(bool)> callb
m_callQueue.append(QueuedCall("postToMQTT", boxId, callback));
return true; // So far it looks we're doing ok... let's return true
}
QString host = "a2addxakg5juii.iot.eu-west-1.amazonaws.com";
QString topic = QString("%1/%2/proxy").arg(boxId).arg(QString(m_identityId));
// This is somehow broken in AWS...
@ -378,14 +731,14 @@ bool AWSClient::postToMQTT(const QString &boxId, std::function<void(bool)> callb
QByteArray payload = QJsonDocument::fromVariant(params).toJson(QJsonDocument::Compact);
QNetworkRequest request("https://" + host + path);
QNetworkRequest request("https://" + m_configs.at(m_usedConfigIndex).mqttEndpoint + path);
request.setRawHeader("content-type", "application/json");
request.setRawHeader("host", host.toUtf8());
request.setRawHeader("host", m_configs.at(m_usedConfigIndex).mqttEndpoint.toUtf8());
SigV4Utils::signRequest(QNetworkAccessManager::PostOperation, request, region, service, m_accessKeyId, m_secretKey, m_sessionToken, payload);
SigV4Utils::signRequest(QNetworkAccessManager::PostOperation, request, m_configs.at(m_usedConfigIndex).region, "iotdata", m_accessKeyId, m_secretKey, m_sessionToken, payload);
// Workaround MQTT broker url weirdness as described above
request.setUrl("https://" + host + path1);
request.setUrl("https://" + m_configs.at(m_usedConfigIndex).mqttEndpoint + path1);
qDebug() << "Posting to MQTT:" << request.url().toString();
qDebug() << "HEADERS:";
@ -430,7 +783,7 @@ void AWSClient::fetchDevices()
return;
}
qDebug() << "Fetching cloud devices";
QUrl url("https://z6368zhf2m.execute-api.eu-west-1.amazonaws.com/dev/devices");
QUrl url(QString("https://%1/users/devices").arg(m_configs.at(m_usedConfigIndex).apiEndpoint));
QNetworkRequest request(url);
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
request.setRawHeader("x-api-idToken", m_idToken);
@ -483,7 +836,8 @@ void AWSClient::refreshAccessToken()
// Non-working block... Enable this if Amazon ever fixes their API...
QUrl url("https://cognito-idp.eu-west-1.amazonaws.com/");
QString host = QString("cognito-idp.%1.amazonaws.com").arg(m_configs.at(m_usedConfigIndex).region);
QUrl url(QString("https://%1/").arg(host));
QUrlQuery query;
query.addQueryItem("Action", "InitiateAuth");
@ -492,12 +846,12 @@ void AWSClient::refreshAccessToken()
QNetworkRequest request(url);
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-amz-json-1.0");
request.setRawHeader("Host", "cognito-idp.eu-west-1.amazonaws.com");
request.setRawHeader("Host", host.toUtf8());
request.setRawHeader("X-Amz-Target", "AWSCognitoIdentityProviderService.InitiateAuth");
QVariantMap params;
params.insert("AuthFlow", "REFRESH_TOKEN_AUTH");
params.insert("ClientId", clientId);
params.insert("ClientId", m_configs.at(m_usedConfigIndex).clientId);
QVariantMap authParams;
authParams.insert("REFRESH_TOKEN", m_refreshToken);
@ -581,7 +935,7 @@ QHash<int, QByteArray> AWSDevices::roleNames() const
AWSDevice *AWSDevices::getDevice(const QString &uuid) const
{
for (int i = 0; i < m_list.count(); i++) {
if (m_list.at(i)->id() == uuid) {
if (QUuid(m_list.at(i)->id()) == QUuid(uuid)) {
return m_list.at(i);
}
}
@ -605,6 +959,25 @@ void AWSDevices::insert(AWSDevice *device)
emit countChanged();
}
void AWSDevices::remove(const QString &uuid)
{
int idx = -1;
for (int i = 0; i < m_list.count(); i++) {
if (m_list.at(i)->id() == uuid) {
idx = i;
break;
}
}
if (idx == -1) {
qWarning() << "Cannot remove AWS with id" << uuid << "as there is no such device";
return;
}
beginRemoveRows(QModelIndex(), idx, idx);
m_list.takeAt(idx)->deleteLater();
endRemoveRows();
emit countChanged();
}
AWSDevice::AWSDevice(const QString &id, const QString &name, bool online, QObject *parent):
QObject (parent),
m_id(id),

View File

@ -46,31 +46,66 @@ public:
Q_INVOKABLE AWSDevice* getDevice(const QString &uuid) const;
Q_INVOKABLE AWSDevice* get(int index) const;
void insert(AWSDevice *device);
void remove(const QString &uuid);
signals:
void countChanged();
private:
QList<AWSDevice*> m_list;
};
class AWSConfiguration {
public:
QByteArray clientId;
QString poolId;
QString identityPoolId;
QString certificateEndpoint;
QString certificateApiKey;
QString certificateVendorId;
QString mqttEndpoint;
QString region;
QString apiEndpoint;
};
class AWSClient : public QObject
{
Q_OBJECT
Q_PROPERTY(bool isLoggedIn READ isLoggedIn NOTIFY isLoggedInChanged)
Q_PROPERTY(QString username READ username NOTIFY isLoggedInChanged)
Q_PROPERTY(bool confirmationPending READ confirmationPending NOTIFY confirmationPendingChanged)
Q_PROPERTY(QByteArray userId READ userId NOTIFY isLoggedInChanged)
Q_PROPERTY(QByteArray idToken READ idToken NOTIFY isLoggedInChanged)
Q_PROPERTY(AWSDevices* awsDevices READ awsDevices CONSTANT)
Q_PROPERTY(int config READ config WRITE setConfig NOTIFY configChanged)
public:
enum LoginError {
LoginErrorNoError,
LoginErrorInvalidUserOrPass,
LoginErrorInvalidCode,
LoginErrorUserExists,
LoginErrorLimitExceeded,
LoginErrorUnknownError
};
Q_ENUM(LoginError)
explicit AWSClient(QObject *parent = nullptr);
bool isLoggedIn() const;
QString username() const;
QByteArray userId() const;
AWSDevices* awsDevices() const;
bool confirmationPending() const;
Q_INVOKABLE void login(const QString &username, const QString &password);
Q_INVOKABLE void logout();
Q_INVOKABLE void signup(const QString &username, const QString &password);
Q_INVOKABLE void confirmRegistration(const QString &code);
Q_INVOKABLE void forgotPassword(const QString &username);
Q_INVOKABLE void confirmForgotPassword(const QString &username, const QString &code, const QString &newPassword);
Q_INVOKABLE void deleteAccount();
Q_INVOKABLE void unpairDevice(const QString &boxId);
Q_INVOKABLE void fetchDevices();
@ -83,10 +118,22 @@ public:
void fetchCertificate(const QString &uuid, std::function<void(const QByteArray &rootCA, const QByteArray &certificate, const QByteArray &publicKey, const QByteArray &privateKey, const QString &endpoint)> callback);
int config() const;
void setConfig(int index);
signals:
void loginResult(LoginError error);
void signupResult(LoginError error);
void confirmationResult(LoginError error);
void forgotPasswordResult(LoginError error);
void confirmForgotPasswordResult(LoginError error);
void isLoggedInChanged();
void confirmationPendingChanged();
void devicesFetched();
void configChanged();
private:
void refreshAccessToken();
void getCredentialsForIdentity(const QString &identityId);
@ -96,9 +143,12 @@ private:
private:
QNetworkAccessManager *m_nam = nullptr;
QString m_userId;
QString m_username;
QString m_password;
bool m_confirmationPending = false;
QByteArray m_accessToken;
QDateTime m_accessTokenExpiry;
QByteArray m_idToken;
@ -114,6 +164,7 @@ private:
class QueuedCall {
public:
QueuedCall(const QString &method): method(method) { }
QueuedCall(const QString &method, const QString &boxId): method(method), boxId(boxId) { }
QueuedCall(const QString &method, const QString &boxId, std::function<void(bool)> callback): method(method), boxId(boxId), callback(callback) {}
QString method;
QString boxId;
@ -122,6 +173,8 @@ private:
QList<QueuedCall> m_callQueue;
QList<AWSConfiguration> m_configs;
int m_usedConfigIndex = 0;
AWSDevices *m_devices;
};

View File

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

View File

@ -92,6 +92,8 @@ QVariant Connections::data(const QModelIndex &index, int role) const
return m_connections.at(index.row())->bearerType();
case RoleSecure:
return m_connections.at(index.row())->secure();
case RoleOnline:
return m_connections.at(index.row())->online();
}
return QVariant();
}
@ -111,10 +113,42 @@ void Connections::addConnection(Connection *connection)
connection->setParent(this);
beginInsertRows(QModelIndex(), m_connections.count(), m_connections.count());
m_connections.append(connection);
connect(connection, &Connection::onlineChanged, this, [this, connection]() {
int idx = m_connections.indexOf(connection);
if (idx < 0) {
return;
}
emit dataChanged(index(idx), index(idx), {RoleOnline});
});
endInsertRows();
emit countChanged();
}
void Connections::removeConnection(Connection *connection)
{
int idx = m_connections.indexOf(connection);
if (idx == -1) {
qWarning() << "Cannot remove connections as it's not in this model";
return;
}
beginRemoveRows(QModelIndex(), idx, idx);
m_connections.takeAt(idx)->deleteLater();
endRemoveRows();
emit countChanged();
}
void Connections::removeConnection(int index)
{
if (index < 0 || index >= m_connections.count()) {
qWarning() << "Index out of range. Not removing any connection";
return;
}
beginRemoveRows(QModelIndex(), index, index);
m_connections.takeAt(index)->deleteLater();
endRemoveRows();
emit countChanged();
}
Connection* Connections::get(int index) const
{
if (index >= 0 && index < m_connections.count()) {
@ -130,6 +164,7 @@ QHash<int, QByteArray> Connections::roleNames() const
roles.insert(RoleBearerType, "bearerType");
roles.insert(RoleName, "name");
roles.insert(RoleSecure, "secure");
roles.insert(RoleOnline, "online");
return roles;
}

View File

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

View File

@ -64,8 +64,24 @@ void DiscoveryModel::addDevice(DiscoveryDevice *device)
emit countChanged();
}
void DiscoveryModel::removeDevice(DiscoveryDevice *device)
{
int idx = m_devices.indexOf(device);
if (idx == -1) {
qWarning() << "Cannot remove DiscoveryDevice" << device << "as its nit in the model";
return;
}
beginRemoveRows(QModelIndex(), idx, idx);
m_devices.takeAt(idx);
endRemoveRows();
emit countChanged();
}
DiscoveryDevice *DiscoveryModel::get(int index) const
{
if (index < 0 || index >= m_devices.count()) {
return nullptr;
}
return m_devices.at(index);
}

View File

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

View File

@ -20,7 +20,13 @@ NymeaDiscovery::NymeaDiscovery(QObject *parent) : QObject(parent)
m_bluetooth = new BluetoothServiceDiscovery(m_discoveryModel, this);
#endif
connect(Engine::instance()->awsClient()->awsDevices(), &AWSDevices::countChanged, this, &NymeaDiscovery::syncCloudDevices);
m_cloudPollTimer.setInterval(5000);
connect(&m_cloudPollTimer, &QTimer::timeout, this, [](){
if (Engine::instance()->awsClient()->isLoggedIn()) {
Engine::instance()->awsClient()->fetchDevices();
}
});
connect(Engine::instance()->awsClient(), &AWSClient::devicesFetched, this, &NymeaDiscovery::syncCloudDevices);
}
bool NymeaDiscovery::discovering() const
@ -44,11 +50,13 @@ void NymeaDiscovery::setDiscovering(bool discovering)
syncCloudDevices();
Engine::instance()->awsClient()->fetchDevices();
}
m_cloudPollTimer.start();
} else {
m_upnp->stopDiscovery();
if (m_bluetooth) {
m_bluetooth->stopDiscovery();
}
m_cloudPollTimer.stop();
}
emit discoveringChanged();
@ -74,11 +82,31 @@ void NymeaDiscovery::syncCloudDevices()
QUrl url;
url.setScheme("cloud");
url.setHost(d->id());
if (!device->connections()->find(url)) {
Connection *conn = new Connection(url, Connection::BearerTypeCloud, true, d->id());
conn->setOnline(d->online());
Connection *conn = device->connections()->find(url);
if (!conn) {
conn = new Connection(url, Connection::BearerTypeCloud, true, d->id());
device->connections()->addConnection(conn);
}
conn->setOnline(d->online());
}
QList<DiscoveryDevice*> devicesToRemove;
for (int i = 0; i < m_discoveryModel->rowCount(); i++) {
DiscoveryDevice *device = m_discoveryModel->get(i);
for (int j = 0; j < device->connections()->rowCount(); j++) {
if (device->connections()->get(j)->bearerType() == Connection::BearerTypeCloud) {
if (Engine::instance()->awsClient()->awsDevices()->getDevice(device->uuid().toString()) == nullptr) {
device->connections()->removeConnection(j);
break;
}
}
}
if (device->connections()->rowCount() == 0) {
devicesToRemove.append(device);
}
}
while (!devicesToRemove.isEmpty()) {
m_discoveryModel->removeDevice(devicesToRemove.takeFirst());
}
}

View File

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

View File

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

View File

@ -12,12 +12,14 @@ ZeroconfDiscovery::ZeroconfDiscovery(DiscoveryModel *discoveryModel, QObject *pa
m_zeroconfJsonRPC = new QZeroConf(this);
connect(m_zeroconfJsonRPC, &QZeroConf::serviceAdded, this, &ZeroconfDiscovery::serviceEntryAdded);
connect(m_zeroconfJsonRPC, &QZeroConf::serviceUpdated, this, &ZeroconfDiscovery::serviceEntryAdded);
connect(m_zeroconfJsonRPC, &QZeroConf::serviceRemoved, this, &ZeroconfDiscovery::serviceEntryRemoved);
m_zeroconfJsonRPC->startBrowser("_jsonrpc._tcp", QAbstractSocket::AnyIPProtocol);
qDebug() << "ZeroConf: Created service browser for _jsonrpc._tcp:" << m_zeroconfJsonRPC->browserExists();
m_zeroconfWebSocket = new QZeroConf(this);
connect(m_zeroconfWebSocket, &QZeroConf::serviceAdded, this, &ZeroconfDiscovery::serviceEntryAdded);
connect(m_zeroconfWebSocket, &QZeroConf::serviceUpdated, this, &ZeroconfDiscovery::serviceEntryAdded);
connect(m_zeroconfWebSocket, &QZeroConf::serviceRemoved, this, &ZeroconfDiscovery::serviceEntryRemoved);
m_zeroconfWebSocket->startBrowser("_ws._tcp", QAbstractSocket::AnyIPProtocol);
qDebug() << "TeroConf: Created service browser for _ws._tcp:" << m_zeroconfWebSocket->browserExists();
#else
@ -84,14 +86,72 @@ void ZeroconfDiscovery::serviceEntryAdded(const QZeroConfService &entry)
} else {
url.setScheme(sslEnabled ? "wss" : "ws");
}
// entry
url.setHost(!entry.ip().isNull() ? entry.ip().toString() : entry.ipv6().toString());
url.setPort(entry.port());
if (!device->connections()->find(url)){
qDebug() << "Zeroconf: Adding new connection to host:" << device->name() << url.toString();
QString displayName = QString("%1:%2").arg(url.host()).arg(url.port());
Connection *connection = new Connection(url, Connection::BearerTypeWifi, sslEnabled, displayName);
connection->setOnline(true);
device->connections()->addConnection(connection);
}
}
void ZeroconfDiscovery::serviceEntryRemoved(const QZeroConfService &entry)
{
if (!entry.name().startsWith("nymea") || (entry.ip().isNull() && entry.ipv6().isNull())) {
return;
}
QString uuid;
bool sslEnabled = false;
QString serverName;
QString version;
foreach (const QByteArray &key, entry.txt().keys()) {
QPair<QString, QString> txtRecord = qMakePair<QString, QString>(key, entry.txt().value(key));
if (!sslEnabled && txtRecord.first == "sslEnabled") {
sslEnabled = (txtRecord.second == "true");
}
if (txtRecord.first == "uuid") {
uuid = txtRecord.second;
}
if (txtRecord.first == "name") {
serverName = txtRecord.second;
}
if (txtRecord.first == "serverVersion") {
version = txtRecord.second;
}
}
// qDebug() << "Zeroconf: Service entry removed" << entry.name();
DiscoveryDevice* device = m_discoveryModel->find(uuid);
if (!device) {
// Nothing to do...
return;
}
QUrl url;
if (entry.type() == "_jsonrpc._tcp") {
url.setScheme(sslEnabled ? "nymeas" : "nymea");
} else {
url.setScheme(sslEnabled ? "wss" : "ws");
}
url.setHost(!entry.ip().isNull() ? entry.ip().toString() : entry.ipv6().toString());
url.setPort(entry.port());
Connection *connection = device->connections()->find(url);
if (!connection){
// Connection url not found...
return;
}
// Ok, now we need to remove it
device->connections()->removeConnection(connection);
// And if there aren't any connections left, remove the entire device
if (device->connections()->rowCount() == 0) {
qDebug() << "Zeroconf: Removing connection from host:" << device->name() << url.toString();
m_discoveryModel->removeDevice(device);
}
}
#endif

View File

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

View File

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

View File

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

View File

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

View File

@ -17,7 +17,7 @@ Page {
model: ListModel {
ListElement { iconSource: "../images/share.svg"; text: qsTr("Configure things"); page: "EditDevicesPage.qml" }
ListElement { iconSource: "../images/magic.svg"; text: qsTr("Magic"); page: "MagicPage.qml" }
ListElement { iconSource: "../images/stock_application.svg"; text: qsTr("App settings"); page: "AppSettingsPage.qml" }
ListElement { iconSource: "../images/stock_application.svg"; text: qsTr("App settings"); page: "appsettings/AppSettingsPage.qml" }
ListElement { iconSource: "../images/settings.svg"; text: qsTr("System settings"); page: "SettingsPage.qml" }
}

View File

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

View File

@ -3,7 +3,7 @@ import QtQuick.Controls 2.1
import QtQuick.Controls.Material 2.1
import QtQuick.Layouts 1.1
import Nymea 1.0
import "components"
import "../components"
Page {
id: root
@ -27,10 +27,29 @@ Page {
spacing: app.margins
Image {
id: logo
Layout.preferredHeight: app.iconSize * 2
Layout.preferredWidth: height
fillMode: Image.PreserveAspectFit
source: "qrc:/styles/%1/logo.svg".arg(styleController.currentStyle)
MouseArea {
anchors.fill: parent
property int clickCounter: 0
onClicked: {
clickCounter++;
if (clickCounter >= 10) {
settings.showHiddenOptions = !settings.showHiddenOptions
var dialog = Qt.createComponent(Qt.resolvedUrl("../components/MeaDialog.qml"));
var text = settings.showHiddenOptions
? qsTr("Developer options are now enabled. If you have found this by accident, it is most likely not of any use for you. It will just enable some nerdy developer gibberish in the app. Tap the icon another 10 times to disable it again.")
: qsTr("Developer options are now disabled.")
var popup = dialog.createObject(app, {headerIcon: "../images/dialog-warning-symbolic.svg", title: qsTr("Howdy cowboy!"), text: text})
popup.open();
clickCounter = 0;
}
}
}
}
GridLayout {
@ -83,60 +102,25 @@ Page {
ColumnLayout {
Layout.fillWidth: true
ItemDelegate {
MeaListItemDelegate {
Layout.fillWidth: true
contentItem: RowLayout {
Label {
Layout.fillWidth: true
text: qsTr("Visit the nymea website")
}
Image {
source: "images/next.svg"
Layout.preferredHeight: parent.height
Layout.preferredWidth: height
}
}
text: qsTr("Visit the nymea website")
onClicked: {
Qt.openUrlExternally("https://nymea.io")
}
}
ItemDelegate {
MeaListItemDelegate {
Layout.fillWidth: true
contentItem: RowLayout {
Label {
Layout.fillWidth: true
text: qsTr("Visit GitHub page")
}
Image {
source: "images/next.svg"
Layout.preferredHeight: parent.height
Layout.preferredWidth: height
}
}
text: qsTr("Visit GitHub page")
onClicked: {
Qt.openUrlExternally("https://github.com/guh/nymea-app")
}
}
ItemDelegate {
MeaListItemDelegate {
Layout.fillWidth: true
contentItem: RowLayout {
Label {
Layout.fillWidth: true
text: qsTr("View license text")
}
Image {
source: "images/next.svg"
Layout.preferredHeight: parent.height
Layout.preferredWidth: height
}
}
text: qsTr("View license text")
onClicked: {
pageStack.push(licenseTextComponent)
}
@ -155,7 +139,7 @@ Page {
Layout.preferredHeight: app.iconSize * 2
Layout.preferredWidth: height
fillMode: Image.PreserveAspectFit
source: "images/Built_with_Qt_RGB_logo_vertical.svg"
source: "qrc:/ui/images/Built_with_Qt_RGB_logo_vertical.svg"
sourceSize.width: app.iconSize * 2
sourceSize.height: app.iconSize * 2
}
@ -166,21 +150,9 @@ Page {
wrapMode: Text.WordWrap
}
}
ItemDelegate {
MeaListItemDelegate {
Layout.fillWidth: true
contentItem: RowLayout {
Label {
Layout.fillWidth: true
text: qsTr("Visit the Qt website")
}
Image {
source: "images/next.svg"
Layout.preferredHeight: parent.height
Layout.preferredWidth: height
}
}
text: qsTr("Visit the Qt website")
onClicked: {
Qt.openUrlExternally("https://www.qt.io")
}

View File

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

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

View File

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

View File

@ -9,15 +9,15 @@
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="96"
height="96"
id="svg4874"
width="90"
height="90"
id="svg6138"
version="1.1"
inkscape:version="0.91+devel r"
viewBox="0 0 96 96.000001"
sodipodi:docname="weather-clouds-symbolic.svg">
viewBox="0 0 90 90.000001"
sodipodi:docname="sync-idle.svg">
<defs
id="defs4876" />
id="defs6140" />
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
@ -25,91 +25,87 @@
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="4.4959994"
inkscape:cx="11.154354"
inkscape:cy="34.719727"
inkscape:zoom="6.3664629"
inkscape:cx="-7.1609628"
inkscape:cy="35.214212"
inkscape:document-units="px"
inkscape:current-layer="g4780"
showgrid="false"
showborder="true"
inkscape:current-layer="g6253"
showgrid="true"
fit-margin-top="0"
fit-margin-left="0"
fit-margin-right="0"
fit-margin-bottom="0"
inkscape:snap-global="true"
inkscape:snap-bbox="true"
inkscape:bbox-paths="true"
inkscape:bbox-nodes="true"
inkscape:snap-bbox-edge-midpoints="true"
inkscape:snap-bbox-midpoints="true"
inkscape:snap-bbox-edge-midpoints="true"
inkscape:bbox-nodes="true"
inkscape:object-paths="true"
inkscape:snap-intersection-paths="true"
inkscape:object-nodes="true"
inkscape:snap-smooth-nodes="true"
inkscape:snap-midpoints="true"
inkscape:snap-others="true"
inkscape:snap-object-midpoints="true"
inkscape:snap-center="true"
showguides="true"
inkscape:guide-bbox="true"
inkscape:snap-global="true">
inkscape:guide-bbox="true">
<inkscape:grid
type="xygrid"
id="grid5451"
empspacing="8" />
<sodipodi:guide
orientation="1,0"
position="8,-8.0000001"
id="guide4063" />
<sodipodi:guide
orientation="1,0"
position="4,-8.0000001"
id="guide4065" />
id="grid6700"
empspacing="6" />
<sodipodi:guide
orientation="0,1"
position="-8,88.000001"
id="guide4067" />
position="62,87"
id="guide4084" />
<sodipodi:guide
orientation="0,1"
position="-8,92.000001"
id="guide4069" />
position="63,84"
id="guide4086" />
<sodipodi:guide
orientation="0,1"
position="104,4"
id="guide4071" />
position="63,81"
id="guide4088" />
<sodipodi:guide
orientation="1,0"
position="3,70"
id="guide4090" />
<sodipodi:guide
orientation="1,0"
position="6,66"
id="guide4092" />
<sodipodi:guide
orientation="1,0"
position="9,59"
id="guide4094" />
<sodipodi:guide
orientation="1,0"
position="87,63"
id="guide4096" />
<sodipodi:guide
orientation="1,0"
position="84,64"
id="guide4098" />
<sodipodi:guide
orientation="1,0"
position="81,55"
id="guide4100" />
<sodipodi:guide
orientation="0,1"
position="-5,8.0000001"
id="guide4073" />
<sodipodi:guide
orientation="1,0"
position="88,-8.0000001"
id="guide4077" />
position="60,3"
id="guide4102" />
<sodipodi:guide
orientation="0,1"
position="-8,84.000001"
id="guide4074" />
position="61,6"
id="guide4104" />
<sodipodi:guide
orientation="1,0"
position="12,-8.0000001"
id="guide4076" />
<sodipodi:guide
orientation="1,0"
position="84,-8.0000001"
id="guide4080" />
<sodipodi:guide
position="48,-8.0000001"
orientation="1,0"
id="guide4170" />
<sodipodi:guide
position="-8,48"
orientation="0,1"
id="guide4172" />
<sodipodi:guide
position="92,-8.0000001"
orientation="1,0"
id="guide4760" />
position="62,9"
id="guide4106" />
</sodipodi:namedview>
<metadata
id="metadata4879">
id="metadata6143">
<rdf:RDF>
<cc:Work
rdf:about="">
@ -124,37 +120,25 @@
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(67.857146,-78.50504)">
transform="translate(-283.57144,-358.79068)">
<g
transform="matrix(0,-1,-1,0,373.50506,516.50504)"
id="g4845"
style="display:inline">
<g
inkscape:export-ydpi="90"
inkscape:export-xdpi="90"
inkscape:export-filename="next01.png"
transform="matrix(-0.9996045,0,0,1,575.94296,-611.00001)"
id="g4778"
inkscape:label="Layer 1">
<g
transform="matrix(-1,0,0,1,575.99999,611)"
id="g4780"
style="display:inline">
<rect
style="color:#000000;display:inline;overflow:visible;visibility:visible;fill:none;stroke:none;stroke-width:4;marker:none;enable-background:accumulate"
id="rect4782"
width="96.037987"
height="96"
x="-438.00244"
y="345.36221"
transform="scale(-1,1)" />
<path
style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;line-height:normal;font-family:sans-serif;-inkscape-font-specification:sans-serif;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;baseline-shift:baseline;text-anchor:start;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;color-interpolation:sRGB;color-interpolation-filters:linearRGB;fill:#808080;fill-opacity:1;stroke:none;stroke-width:4.00000048;marker:none;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
d="M 51.539062 16.888672 C 39.655652 16.888672 29.559451 24.212821 24.738281 34.675781 L 24.416016 35.376953 L 23.646484 35.433594 C 12.678934 36.232468 3.9980469 45.714566 3.9980469 57.255859 C 3.9980469 69.316737 13.439055 79.21875 25.109375 79.21875 L 51.539062 79.21875 L 76.175781 79.21875 C 84.926381 79.21875 92 71.787763 92 62.779297 C 92 55.94799 87.899411 50.217256 82.082031 47.746094 L 81.324219 47.423828 L 81.287109 46.603516 C 80.540729 30.134022 67.596276 16.888672 51.541016 16.888672 L 51.539062 16.888672 z M 51.539062 20.888672 C 65.393253 20.888672 76.634896 32.305371 77.291016 46.783203 L 77.289062 46.779297 L 77.4375 50.117188 L 80.515625 51.427734 C 84.906955 53.293126 87.998047 57.547827 87.998047 62.779297 C 87.998047 69.694971 82.658528 75.21875 76.173828 75.21875 L 51.539062 75.21875 L 25.109375 75.21875 C 15.702915 75.21875 7.9980469 67.230513 7.9980469 57.255859 C 7.9980469 47.719033 15.103887 40.06517 23.935547 39.421875 L 23.9375 39.421875 L 27.064453 39.191406 L 28.371094 36.349609 C 32.601004 27.169791 41.300243 20.888672 51.539062 20.888672 z "
transform="matrix(0,-1,-1.0003957,0,438.00245,441.36222)"
id="path4181" />
</g>
</g>
id="g6253"
inkscape:export-filename="planemode01.png"
inkscape:export-xdpi="90"
inkscape:export-ydpi="90"
transform="matrix(-1,0,0,1,547.57143,-1341.5715)">
<rect
style="fill:none;stroke:none"
id="rect6257"
width="90"
height="90"
x="174"
y="1700.3622" />
<path
style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;font-variant-ligatures:none;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-feature-settings:normal;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;baseline-shift:baseline;text-anchor:start;white-space:normal;shape-padding:0;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#808080;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:4;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
d="m 212.5,1714.3613 c -11.29815,0 -20.5,9.2019 -20.5,20.5 l 0,0.01 0,0.01 c 0.002,0.2497 0.0454,0.4967 0.0566,0.7461 -10.15029,1.2279 -18.0494,9.7961 -18.05664,20.2363 0,11.2982 9.20185,20.5 20.5,20.5 l 49,0 c 11.29815,0 20.5,-9.2018 20.5,-20.5 l 0,-0 c -0.0116,-8.9301 -5.844,-16.696 -14.21094,-19.4063 0.0644,-0.5317 0.21041,-1.0527 0.21094,-1.5898 l 0,-0 c 0,-7.4321 -6.06785,-13.5 -13.5,-13.5 l -0.002,0 -0.002,0 c -2.55665,0 -4.93658,0.9271 -7.07421,2.2754 -3.76777,-5.6712 -10.02107,-9.2653 -16.91993,-9.2754 l -0.002,0 z m -0.002,4 0.002,0 c 6.1355,0.01 11.75025,3.4089 14.59766,8.8418 l 1.18945,2.2676 1.91211,-1.7031 c 1.73553,-1.5457 3.97683,-2.4018 6.30078,-2.4063 5.27039,0 9.5,4.2296 9.5,9.5 -0.001,0.8309 -0.11279,1.6591 -0.33008,2.4629 l -0.54297,2.0117 2.03125,0.461 c 7.51333,1.7082 12.82999,8.3597 12.8418,16.0644 0,7e-4 0,0 0,0 0,6e-4 0,0 0,0 -0.002,9.1346 -7.36493,16.4961 -16.5,16.4961 l -49,0 c -9.13573,0 -16.49893,-7.3625 -16.5,-16.498 l 0,-0 c 0.007,-9.023 7.2045,-16.3348 16.22656,-16.4843 l 2.27539,-0.037 -0.33007,-2.25 c -0.10801,-0.7397 -0.16511,-1.4866 -0.17188,-2.2343 0.003,-9.133 7.36456,-16.4931 16.49805,-16.4942 z"
id="path4154"
inkscape:connector-curvature="0" />
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 6.3 KiB

After

Width:  |  Height:  |  Size: 6.0 KiB

View File

@ -11,21 +11,16 @@ Page {
onBackPressed: pageStack.pop();
}
Connections {
target: Engine.basicConfiguration
onCloudEnabledChanged: {
if (Engine.jsonRpcClient.cloudConnectionState === JsonRpcClient.CloudConnectionStateUnconfigured) {
Engine.deployCertificate();
}
}
}
Item {
id: d
property bool deploymentStarted: false
Connections {
target: Engine.jsonRpcClient
onCloudConnectionStateChanged: {
if (Engine.awsClient.isLoggedIn && Engine.awsClient.awsDevices.getDevice(Engine.jsonRpcClient.serverUuid) === null) {
print("Pairing user and box...")
Engine.jsonRpcClient.setupRemoteAccess(Engine.awsClient.idToken, Engine.awsClient.userId);
Connections {
target: Engine.jsonRpcClient
onCloudConnectionStateChanged: {
if (Engine.jsonRpcClient.cloudConnectionState == JsonRpcClient.CloudConnectionStateConnected) {
d.deploymentStarted = false;
}
}
}
}
@ -51,33 +46,67 @@ Page {
}
}
ThinDivider {}
RowLayout {
Layout.fillWidth: true
Layout.leftMargin: app.margins
Layout.rightMargin: app.margins
visible: Engine.basicConfiguration.cloudEnabled
BusyIndicator {
visible: Engine.jsonRpcClient.cloudConnectionState == JsonRpcClient.CloudConnectionStateUnconfigured ||
Engine.jsonRpcClient.cloudConnectionState == JsonRpcClient.CloudConnectionStateConnecting
ColorIcon {
Layout.preferredHeight: busyIndicator.height
Layout.preferredWidth: height
name: Engine.jsonRpcClient.cloudConnectionState === JsonRpcClient.CloudConnectionStateConnected
? "../images/cloud.svg"
: Engine.jsonRpcClient.cloudConnectionState === JsonRpcClient.CloudConnectionStateUnconfigured
? "../images/cloud-error.svg"
: "../images/cloud-offline.svg"
}
Label {
Layout.fillWidth: true
wrapMode: Text.WordWrap
text: {
switch (Engine.jsonRpcClient.cloudConnectionState) {
case JsonRpcClient.CloudConnectionStateDisabled:
return ""
return qsTr("This box is not connected to %1:cloud").arg(app.systemName)
case JsonRpcClient.CloudConnectionStateUnconfigured:
return qsTr("Configuring the box to connect to nymea:cloud...");
if (d.deploymentStarted) {
return qsTr("Registering box in %1:cloud...").arg(app.systemName)
}
return qsTr("This box is not configured to connect to %1:cloud.").arg(app.systemName);
case JsonRpcClient.CloudConnectionStateConnecting:
return qsTr("Connecting the box to nymea:cloud...");
return qsTr("Connecting the box to %1:cloud...").arg(app.systemName);
case JsonRpcClient.CloudConnectionStateConnected:
return qsTr("The box is connected to nymea:cloud.");
return qsTr("The box is connected to %1:cloud.").arg(app.systemName);
}
return Engine.jsonRpcClient.cloudConnectionState
}
}
BusyIndicator {
id: busyIndicator
visible: (Engine.jsonRpcClient.cloudConnectionState == JsonRpcClient.CloudConnectionStateUnconfigured && d.deploymentStarted) ||
Engine.jsonRpcClient.cloudConnectionState == JsonRpcClient.CloudConnectionStateConnecting
}
}
Label {
Layout.fillWidth: true
Layout.leftMargin: app.margins; Layout.rightMargin: app.margins
visible: Engine.jsonRpcClient.cloudConnectionState === JsonRpcClient.CloudConnectionStateUnconfigured && !d.deploymentStarted
text: qsTr("This box is not configured to access the %1:cloud. In order for a box to connect to %1:cloud it needs to be registered first.").arg(app.systemName)
wrapMode: Text.WordWrap
}
Button {
Layout.fillWidth: true
Layout.leftMargin: app.margins; Layout.rightMargin: app.margins
visible: Engine.jsonRpcClient.cloudConnectionState === JsonRpcClient.CloudConnectionStateUnconfigured && !d.deploymentStarted
text: qsTr("Register box")
onClicked: {
d.deploymentStarted = true
Engine.deployCertificate();
}
}

@ -1 +1 @@
Subproject commit f6e2d9b3b208362159dd22a4eb11527db25d8760
Subproject commit 3b97bc60bbaf1ae5d4ced7eb1586d2d20fe5b0ab