mirror of
https://github.com/cagnulein/qdomyos-zwift.git
synced 2026-02-18 00:17:41 +01:00
Compare commits
35 Commits
android36
...
build-1016
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
66513305f5 | ||
|
|
8245d76924 | ||
|
|
b2623a9439 | ||
|
|
a522ded028 | ||
|
|
b905e61321 | ||
|
|
e1c237a781 | ||
|
|
3a7b205ebc | ||
|
|
7d23a95355 | ||
|
|
a283189de3 | ||
|
|
4641a1174e | ||
|
|
5382016c1f | ||
|
|
4d75fe4716 | ||
|
|
94e60a934b | ||
|
|
27de1ab726 | ||
|
|
4eca29b090 | ||
|
|
d6ea58be55 | ||
|
|
560e718119 | ||
|
|
0a4784396c | ||
|
|
7023ef3f4c | ||
|
|
58ea93252a | ||
|
|
0d209bacd3 | ||
|
|
0abf84d57c | ||
|
|
9eda2c932e | ||
|
|
299139cdac | ||
|
|
7266e57ee0 | ||
|
|
2eafe4b17a | ||
|
|
255d657075 | ||
|
|
afc18c8670 | ||
|
|
618432428a | ||
|
|
6e3b7e94f6 | ||
|
|
eafe480ab6 | ||
|
|
86ae28e15e | ||
|
|
86f5c22fe2 | ||
|
|
38f4bc6fda | ||
|
|
483624cd3b |
@@ -4233,7 +4233,7 @@
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CODE_SIGN_ENTITLEMENTS = "../src/ios/qdomyos-zwift.entitlements";
|
||||
COPY_PHASE_STRIP = NO;
|
||||
CURRENT_PROJECT_VERSION = 1011;
|
||||
CURRENT_PROJECT_VERSION = 1016;
|
||||
DEVELOPMENT_TEAM = 6335M7T29D;
|
||||
ENABLE_BITCODE = NO;
|
||||
GCC_PREPROCESSOR_DEFINITIONS = "ADB_HOST=1";
|
||||
@@ -4427,7 +4427,7 @@
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CODE_SIGN_ENTITLEMENTS = "../src/ios/qdomyos-zwift.entitlements";
|
||||
COPY_PHASE_STRIP = NO;
|
||||
CURRENT_PROJECT_VERSION = 1011;
|
||||
CURRENT_PROJECT_VERSION = 1016;
|
||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||
DEVELOPMENT_TEAM = 6335M7T29D;
|
||||
ENABLE_BITCODE = NO;
|
||||
@@ -4657,7 +4657,7 @@
|
||||
CODE_SIGN_ENTITLEMENTS = "watchkit Extension/WatchKit Extension.entitlements";
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1011;
|
||||
CURRENT_PROJECT_VERSION = 1016;
|
||||
DEVELOPMENT_TEAM = 6335M7T29D;
|
||||
ENABLE_BITCODE = YES;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
@@ -4753,7 +4753,7 @@
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = "watchkit Extension/WatchKit Extension.entitlements";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1011;
|
||||
CURRENT_PROJECT_VERSION = 1016;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
DEVELOPMENT_TEAM = 6335M7T29D;
|
||||
ENABLE_BITCODE = YES;
|
||||
@@ -4845,7 +4845,7 @@
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = "watchkit Extension/WatchKit Extension.entitlements";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1011;
|
||||
CURRENT_PROJECT_VERSION = 1016;
|
||||
DEVELOPMENT_ASSET_PATHS = "\"watchkit Extension/Preview Content\"";
|
||||
ENABLE_BITCODE = YES;
|
||||
ENABLE_PREVIEWS = YES;
|
||||
@@ -4961,7 +4961,7 @@
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = "watchkit Extension/WatchKit Extension.entitlements";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1011;
|
||||
CURRENT_PROJECT_VERSION = 1016;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
DEVELOPMENT_ASSET_PATHS = "\"watchkit Extension/Preview Content\"";
|
||||
ENABLE_BITCODE = YES;
|
||||
|
||||
26
src/OAuth2.h
Normal file
26
src/OAuth2.h
Normal file
@@ -0,0 +1,26 @@
|
||||
#ifndef OAUTH2_H
|
||||
#define OAUTH2_H
|
||||
|
||||
#include <QString>
|
||||
#include <QTextStream>
|
||||
|
||||
struct OAuth2Parameter {
|
||||
QString responseType = QStringLiteral("code");
|
||||
QString approval_prompt = QStringLiteral("force");
|
||||
|
||||
inline bool isEmpty() const { return responseType.isEmpty() && approval_prompt.isEmpty(); }
|
||||
|
||||
QString toString() const {
|
||||
QString msg;
|
||||
QTextStream out(&msg);
|
||||
out << QStringLiteral("OAuth2Parameter{\n") << QStringLiteral("responseType: ") << this->responseType
|
||||
<< QStringLiteral("\n") << QStringLiteral("approval_prompt: ") << this->approval_prompt
|
||||
<< QStringLiteral("\n");
|
||||
return msg;
|
||||
}
|
||||
};
|
||||
|
||||
#define _STR(x) #x
|
||||
#define STRINGIFY(x) _STR(x)
|
||||
|
||||
#endif // OAUTH2_H
|
||||
58
src/WebPelotonAuth.qml
Normal file
58
src/WebPelotonAuth.qml
Normal file
@@ -0,0 +1,58 @@
|
||||
import QtQuick 2.12
|
||||
import QtQuick.Controls 2.5
|
||||
import QtQuick.Controls.Material 2.12
|
||||
import QtQuick.Dialogs 1.0
|
||||
import QtGraphicalEffects 1.12
|
||||
import Qt.labs.settings 1.0
|
||||
import QtMultimedia 5.15
|
||||
import QtQuick.Layouts 1.3
|
||||
import QtWebView 1.1
|
||||
|
||||
Item {
|
||||
anchors.fill: parent
|
||||
height: parent.height
|
||||
width: parent.width
|
||||
visible: true
|
||||
|
||||
WebView {
|
||||
anchors.fill: parent
|
||||
height: parent.height
|
||||
width: parent.width
|
||||
visible: !rootItem.pelotonPopupVisible
|
||||
url: rootItem.getPelotonAuthUrl
|
||||
}
|
||||
|
||||
Popup {
|
||||
id: popupPelotonConnectedWeb
|
||||
parent: Overlay.overlay
|
||||
enabled: rootItem.pelotonPopupVisible
|
||||
onEnabledChanged: { if(rootItem.pelotonPopupVisible) popupPelotonConnectedWeb.open() }
|
||||
onClosed: { rootItem.pelotonPopupVisible = false; }
|
||||
|
||||
x: Math.round((parent.width - width) / 2)
|
||||
y: Math.round((parent.height - height) / 2)
|
||||
width: 380
|
||||
height: 120
|
||||
modal: true
|
||||
focus: true
|
||||
palette.text: "white"
|
||||
closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside
|
||||
enter: Transition
|
||||
{
|
||||
NumberAnimation { property: "opacity"; from: 0.0; to: 1.0 }
|
||||
}
|
||||
exit: Transition
|
||||
{
|
||||
NumberAnimation { property: "opacity"; from: 1.0; to: 0.0 }
|
||||
}
|
||||
Column {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
Label {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
width: 370
|
||||
height: 120
|
||||
text: qsTr("Your Peloton account is now connected!<br><br>Restart the app to apply this!")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -51,8 +51,6 @@ using namespace std::chrono_literals;
|
||||
#pragma message "DEFINE STRAVA_CLIENT_ID!!!"
|
||||
#endif
|
||||
#endif
|
||||
#define _STR(x) #x
|
||||
#define STRINGIFY(x) _STR(x)
|
||||
#define STRAVA_CLIENT_ID_S STRINGIFY(STRAVA_CLIENT_ID)
|
||||
|
||||
DataObject::DataObject(const QString &name, const QString &icon, const QString &value, bool writable, const QString &id,
|
||||
@@ -550,6 +548,9 @@ homeform::homeform(QQmlApplicationEngine *engine, bluetooth *bl) {
|
||||
connect(pelotonHandler, &peloton::workoutChanged, this, &homeform::pelotonWorkoutChanged);
|
||||
connect(pelotonHandler, &peloton::loginState, this, &homeform::pelotonLoginState);
|
||||
connect(pelotonHandler, &peloton::pzpLoginState, this, &homeform::pzpLoginState);
|
||||
connect(pelotonHandler, &peloton::pelotonAuthUrlChanged, this, &homeform::pelotonAuthUrlChanged);
|
||||
connect(pelotonHandler, &peloton::pelotonWebVisibleChanged, this, &homeform::pelotonWebVisibleChanged);
|
||||
connect(stack, SIGNAL(peloton_connect_clicked()), pelotonHandler, SLOT(peloton_connect_clicked()));
|
||||
|
||||
// copying bundles zwo files in the right path if necessary
|
||||
QDirIterator itZwo(":/zwo/");
|
||||
@@ -6254,22 +6255,6 @@ QStringList homeform::bluetoothDevices() {
|
||||
|
||||
QStringList homeform::metrics() { return bluetoothdevice::metrics(); }
|
||||
|
||||
struct OAuth2Parameter {
|
||||
QString responseType = QStringLiteral("code");
|
||||
QString approval_prompt = QStringLiteral("force");
|
||||
|
||||
inline bool isEmpty() const { return responseType.isEmpty() && approval_prompt.isEmpty(); }
|
||||
|
||||
QString toString() const {
|
||||
QString msg;
|
||||
QTextStream out(&msg);
|
||||
out << QStringLiteral("OAuth2Parameter{\n") << QStringLiteral("responseType: ") << this->responseType
|
||||
<< QStringLiteral("\n") << QStringLiteral("approval_prompt: ") << this->approval_prompt
|
||||
<< QStringLiteral("\n");
|
||||
return msg;
|
||||
}
|
||||
};
|
||||
|
||||
QAbstractOAuth::ModifyParametersFunction
|
||||
homeform::buildModifyParametersFunction(const QUrl &clientIdentifier, const QUrl &clientIdentifierSharedKey) {
|
||||
return [clientIdentifier, clientIdentifierSharedKey](QAbstractOAuth::Stage stage, QVariantMap *parameters) {
|
||||
@@ -6682,6 +6667,14 @@ void homeform::setGeneralPopupVisible(bool value) {
|
||||
emit generalPopupVisibleChanged(m_generalPopupVisible);
|
||||
}
|
||||
|
||||
bool homeform::pelotonPopupVisible() { return m_pelotonPopupVisible; }
|
||||
|
||||
void homeform::setPelotonPopupVisible(bool value) {
|
||||
|
||||
m_pelotonPopupVisible = value;
|
||||
emit pelotonPopupVisibleChanged(m_pelotonPopupVisible);
|
||||
}
|
||||
|
||||
bool homeform::licensePopupVisible() { return m_LicensePopupVisible; }
|
||||
|
||||
void homeform::setLicensePopupVisible(bool value) {
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
#include "bluetooth.h"
|
||||
#include "fit_profile.hpp"
|
||||
#include "gpx.h"
|
||||
#include "OAuth2.h"
|
||||
#include "peloton.h"
|
||||
#include "qmdnsengine/browser.h"
|
||||
#include "qmdnsengine/cache.h"
|
||||
@@ -150,6 +151,8 @@ class homeform : public QObject {
|
||||
Q_PROPERTY(QStringList tile_order READ tile_order NOTIFY tile_orderChanged)
|
||||
Q_PROPERTY(bool generalPopupVisible READ generalPopupVisible NOTIFY generalPopupVisibleChanged WRITE
|
||||
setGeneralPopupVisible)
|
||||
Q_PROPERTY(bool pelotonPopupVisible READ pelotonPopupVisible NOTIFY pelotonPopupVisibleChanged WRITE
|
||||
setPelotonPopupVisible)
|
||||
Q_PROPERTY(bool licensePopupVisible READ licensePopupVisible NOTIFY licensePopupVisibleChanged WRITE
|
||||
setLicensePopupVisible)
|
||||
Q_PROPERTY(bool mapsVisible READ mapsVisible NOTIFY mapsVisibleChanged WRITE setMapsVisible)
|
||||
@@ -193,6 +196,10 @@ class homeform : public QObject {
|
||||
Q_PROPERTY(QString getStravaAuthUrl READ getStravaAuthUrl NOTIFY stravaAuthUrlChanged)
|
||||
Q_PROPERTY(bool stravaWebVisible READ stravaWebVisible NOTIFY stravaWebVisibleChanged)
|
||||
|
||||
QString getPelotonAuthUrl() { if(!pelotonHandler) return ""; return pelotonHandler->pelotonAuthUrl; }
|
||||
bool pelotonWebVisible() { if(!pelotonHandler) return false; return pelotonHandler->pelotonAuthWebVisible; }
|
||||
Q_PROPERTY(QString getPelotonAuthUrl READ getPelotonAuthUrl NOTIFY pelotonAuthUrlChanged)
|
||||
Q_PROPERTY(bool pelotonWebVisible READ pelotonWebVisible NOTIFY pelotonWebVisibleChanged)
|
||||
|
||||
public:
|
||||
static homeform *singleton() { return m_singleton; }
|
||||
@@ -446,6 +453,7 @@ class homeform : public QObject {
|
||||
bool stravaUploadRequested() { return m_stravaUploadRequested; }
|
||||
void setPelotonProvider(const QString &value) { m_pelotonProvider = value; }
|
||||
bool generalPopupVisible();
|
||||
bool pelotonPopupVisible();
|
||||
bool licensePopupVisible();
|
||||
bool mapsVisible();
|
||||
bool videoIconVisible();
|
||||
@@ -501,6 +509,7 @@ class homeform : public QObject {
|
||||
m_stravaUploadRequested = value;
|
||||
}
|
||||
void setGeneralPopupVisible(bool value);
|
||||
void setPelotonPopupVisible(bool value);
|
||||
int workout_sample_points() { return Session.count(); }
|
||||
int preview_workout_points();
|
||||
|
||||
@@ -697,6 +706,7 @@ class homeform : public QObject {
|
||||
QString m_info = QStringLiteral("Connecting...");
|
||||
bool m_labelHelp = true;
|
||||
bool m_generalPopupVisible = false;
|
||||
bool m_pelotonPopupVisible = false;
|
||||
bool m_LicensePopupVisible = false;
|
||||
bool m_MapsVisible = false;
|
||||
bool m_VideoIconVisible = false;
|
||||
@@ -898,6 +908,7 @@ class homeform : public QObject {
|
||||
void toastRequestedChanged(QString value);
|
||||
void stravaUploadRequestedChanged(bool value);
|
||||
void generalPopupVisibleChanged(bool value);
|
||||
void pelotonPopupVisibleChanged(bool value);
|
||||
void licensePopupVisibleChanged(bool value);
|
||||
void videoIconVisibleChanged(bool value);
|
||||
void videoVisibleChanged(bool value);
|
||||
@@ -923,6 +934,8 @@ class homeform : public QObject {
|
||||
void previewWorkoutTagsChanged(QString value);
|
||||
void stravaAuthUrlChanged(QString value);
|
||||
void stravaWebVisibleChanged(bool value);
|
||||
void pelotonAuthUrlChanged(QString value);
|
||||
void pelotonWebVisibleChanged(bool value);
|
||||
|
||||
void workoutEventStateChanged(bluetoothdevice::WORKOUT_EVENT_STATE state);
|
||||
|
||||
|
||||
@@ -33,5 +33,6 @@
|
||||
<file>icons/video.png</file>
|
||||
<file>icons/mini-display.png</file>
|
||||
<file>icons/btn_strava_connectwith_orange.png</file>
|
||||
<file>icons/Button_Connect_Rect_DarkMode.png</file>
|
||||
</qresource>
|
||||
</RCC>
|
||||
|
||||
BIN
src/icons/Button_Connect_Rect_DarkMode.png
Normal file
BIN
src/icons/Button_Connect_Rect_DarkMode.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.5 KiB |
84
src/main.qml
84
src/main.qml
@@ -30,6 +30,7 @@ ApplicationWindow {
|
||||
signal fit_save_clicked()
|
||||
signal refresh_bluetooth_devices_clicked()
|
||||
signal strava_connect_clicked()
|
||||
signal peloton_connect_clicked()
|
||||
signal loadSettings(url name)
|
||||
signal saveSettings(url name)
|
||||
signal deleteSettings(url name)
|
||||
@@ -52,6 +53,8 @@ ApplicationWindow {
|
||||
property string profile_name: "default"
|
||||
property string theme_status_bar_background_color: "#800080"
|
||||
property bool volume_change_gears: false
|
||||
property string peloton_username: "username"
|
||||
property string peloton_password: "password"
|
||||
}
|
||||
|
||||
Store {
|
||||
@@ -185,6 +188,33 @@ ApplicationWindow {
|
||||
}
|
||||
}
|
||||
|
||||
MessageDialog {
|
||||
id: popupPelotonAuth
|
||||
text: "Peloton Authentication Change"
|
||||
informativeText: "Peloton has moved to a new authentication system. Username and password are no longer required.\n\nWould you like to switch to the new authentication method now?"
|
||||
buttons: (MessageDialog.Yes | MessageDialog.No)
|
||||
onYesClicked: {
|
||||
settings.peloton_username = "username"
|
||||
settings.peloton_password = "password"
|
||||
stackView.push("WebPelotonAuth.qml")
|
||||
peloton_connect_clicked()
|
||||
}
|
||||
onNoClicked: this.visible = false
|
||||
visible: false
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: pelotonAuthCheck
|
||||
interval: 1000 // 1 second delay after startup
|
||||
running: true
|
||||
repeat: false
|
||||
onTriggered: {
|
||||
if (settings.peloton_password !== "password") {
|
||||
popupPelotonAuth.visible = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Popup {
|
||||
id: popupClassificaHelper
|
||||
parent: Overlay.overlay
|
||||
@@ -345,6 +375,40 @@ ApplicationWindow {
|
||||
}
|
||||
}
|
||||
|
||||
Popup {
|
||||
id: popupPelotonConnected
|
||||
parent: Overlay.overlay
|
||||
enabled: rootItem.pelotonPopupVisible
|
||||
onEnabledChanged: { if(rootItem.pelotonPopupVisible) popupPelotonConnected.open() }
|
||||
onClosed: { rootItem.pelotonPopupVisible = false; }
|
||||
|
||||
x: Math.round((parent.width - width) / 2)
|
||||
y: Math.round((parent.height - height) / 2)
|
||||
width: 380
|
||||
height: 120
|
||||
modal: true
|
||||
focus: true
|
||||
palette.text: "white"
|
||||
closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside
|
||||
enter: Transition
|
||||
{
|
||||
NumberAnimation { property: "opacity"; from: 0.0; to: 1.0 }
|
||||
}
|
||||
exit: Transition
|
||||
{
|
||||
NumberAnimation { property: "opacity"; from: 1.0; to: 0.0 }
|
||||
}
|
||||
Column {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
Label {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
width: 370
|
||||
height: 120
|
||||
text: qsTr("Your Peloton account is now connected!<br><br>Restart the app to apply this change!")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: popupLicenseAutoClose
|
||||
interval: 120000; running: rootItem.licensePopupVisible; repeat: false
|
||||
@@ -645,6 +709,9 @@ ApplicationWindow {
|
||||
toolButtonLoadSettings.visible = true;
|
||||
toolButtonSaveSettings.visible = true;
|
||||
stackView.push("settings.qml")
|
||||
stackView.currentItem.peloton_connect_clicked.connect(function() {
|
||||
peloton_connect_clicked()
|
||||
});
|
||||
drawer.close()
|
||||
}
|
||||
}
|
||||
@@ -799,6 +866,23 @@ ApplicationWindow {
|
||||
}
|
||||
}
|
||||
|
||||
ItemDelegate {
|
||||
Image {
|
||||
anchors.left: parent.left;
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
source: "icons/icons/Button_Connect_Rect_DarkMode.png"
|
||||
fillMode: Image.PreserveAspectFit
|
||||
visible: true
|
||||
width: parent.width
|
||||
}
|
||||
width: parent.width
|
||||
onClicked: {
|
||||
stackView.push("WebPelotonAuth.qml")
|
||||
peloton_connect_clicked()
|
||||
drawer.close()
|
||||
}
|
||||
}
|
||||
|
||||
FileDialog {
|
||||
id: fileDialogGPX
|
||||
title: "Please choose a file"
|
||||
|
||||
514
src/peloton.cpp
514
src/peloton.cpp
@@ -1,3 +1,13 @@
|
||||
#if __has_include("secret.h")
|
||||
#include "secret.h"
|
||||
#else
|
||||
#if defined(WIN32)
|
||||
#pragma message("PELOTON API WILL NOT WORK!!!")
|
||||
#else
|
||||
#warning "PELOTON API WILL NOT WORK!!!"
|
||||
#endif
|
||||
#endif
|
||||
#include "homeform.h"
|
||||
#include "peloton.h"
|
||||
#include <chrono>
|
||||
|
||||
@@ -5,6 +15,8 @@ using namespace std::chrono_literals;
|
||||
|
||||
const bool log_request = true;
|
||||
|
||||
#define RAWHEADER request.setHeader(QNetworkRequest::ContentTypeHeader, QStringLiteral("application/json"));request.setHeader(QNetworkRequest::UserAgentHeader, QStringLiteral("qdomyos-zwift"));request.setRawHeader(QByteArray("Authorization"), QByteArray("Bearer ") + settings.value(QZSettings::peloton_accesstoken, QZSettings::default_peloton_accesstoken).toString().toUtf8());
|
||||
|
||||
peloton::peloton(bluetooth *bl, QObject *parent) : QObject(parent) {
|
||||
|
||||
QSettings settings;
|
||||
@@ -12,6 +24,9 @@ peloton::peloton(bluetooth *bl, QObject *parent) : QObject(parent) {
|
||||
mgr = new QNetworkAccessManager(this);
|
||||
timer = new QTimer(this);
|
||||
|
||||
//peloton_connect_clicked();
|
||||
peloton_refreshtoken();
|
||||
|
||||
// only for test purpose
|
||||
/*
|
||||
current_image_downloaded =
|
||||
@@ -19,9 +34,9 @@ peloton::peloton(bluetooth *bl, QObject *parent) : QObject(parent) {
|
||||
"img_1646099287_a620f71b3d6740718457b21769a7ed46.png"));
|
||||
*/
|
||||
|
||||
if (!settings.value(QZSettings::peloton_username, QZSettings::default_peloton_username)
|
||||
if (!settings.value(QZSettings::peloton_accesstoken, QZSettings::default_peloton_accesstoken)
|
||||
.toString()
|
||||
.compare(QStringLiteral("username"))) {
|
||||
.length()) {
|
||||
qDebug() << QStringLiteral("invalid peloton credentials");
|
||||
return;
|
||||
}
|
||||
@@ -627,21 +642,16 @@ void peloton::startEngine() {
|
||||
QSettings settings;
|
||||
timer->stop();
|
||||
connect(mgr, &QNetworkAccessManager::finished, this, &peloton::login_onfinish);
|
||||
QUrl url(QStringLiteral("https://api.onepeloton.com/auth/login"));
|
||||
|
||||
QUrl url(QStringLiteral("https://api-3p.onepeloton.com/api/v1/me"));
|
||||
qDebug() << "peloton::getMe" << url;
|
||||
QNetworkRequest request(url);
|
||||
|
||||
request.setHeader(QNetworkRequest::ContentTypeHeader, QStringLiteral("application/json"));
|
||||
request.setHeader(QNetworkRequest::UserAgentHeader, QStringLiteral("qdomyos-zwift"));
|
||||
|
||||
QJsonObject obj;
|
||||
obj[QStringLiteral("username_or_email")] =
|
||||
settings.value(QZSettings::peloton_username, QZSettings::default_peloton_username).toString();
|
||||
obj[QStringLiteral("password")] =
|
||||
settings.value(QZSettings::peloton_password, QZSettings::default_peloton_password).toString();
|
||||
QJsonDocument doc(obj);
|
||||
QByteArray data = doc.toJson();
|
||||
|
||||
mgr->post(request, data);
|
||||
RAWHEADER
|
||||
|
||||
qDebug() << settings.value(QZSettings::peloton_accesstoken, QZSettings::default_peloton_accesstoken).toString().toLatin1() << request.rawHeader(QByteArray("authorization"));
|
||||
|
||||
mgr->get(request);
|
||||
}
|
||||
|
||||
void peloton::login_onfinish(QNetworkReply *reply) {
|
||||
@@ -667,9 +677,11 @@ void peloton::login_onfinish(QNetworkReply *reply) {
|
||||
return;
|
||||
}
|
||||
|
||||
user_id = document[QStringLiteral("user_id")].toString();
|
||||
total_workout = document[QStringLiteral("user_data")][QStringLiteral("total_workouts")].toInt();
|
||||
user_id = document[QStringLiteral("id")].toString();
|
||||
total_workout = document[QStringLiteral("total_workouts")].toInt();
|
||||
|
||||
qDebug() << "user_id" << user_id << "total workout" << total_workout;
|
||||
|
||||
emit loginState(!user_id.isEmpty());
|
||||
|
||||
getWorkoutList(1);
|
||||
@@ -1391,7 +1403,175 @@ void peloton::performance_onfinish(QNetworkReply *reply) {
|
||||
QJsonArray segment_list = json[QStringLiteral("segment_list")].toArray();
|
||||
trainrows.clear();
|
||||
|
||||
if (!target_metrics_performance_data.isEmpty() && bluetoothManager->device() &&
|
||||
if(!target_metrics_performance_data.isEmpty() && bluetoothManager->device() &&
|
||||
bluetoothManager->device()->deviceType() == bluetoothdevice::BIKE) {
|
||||
QJsonArray targetMetrics = target_metrics_performance_data[QStringLiteral("target_metrics")].toArray();
|
||||
|
||||
if (targetMetrics.count() > 0)
|
||||
trainrows.reserve(targetMetrics.count());
|
||||
|
||||
QSettings settings;
|
||||
QString difficulty =
|
||||
settings.value(QZSettings::peloton_difficulty, QZSettings::default_peloton_difficulty).toString();
|
||||
bool powerZoneFound = false;
|
||||
|
||||
for (int i = 0; i < targetMetrics.count(); i++) {
|
||||
QJsonObject targetMetric = targetMetrics.at(i).toObject();
|
||||
QJsonObject offsets = targetMetric[QStringLiteral("offsets")].toObject();
|
||||
QJsonArray metrics = targetMetric[QStringLiteral("metrics")].toArray();
|
||||
|
||||
// Find resistance and cadence metrics
|
||||
int lowerResistance = 0, upperResistance = 0, lowerCadence = 0, upperCadence = 0;
|
||||
for (QJsonValue metricValue : metrics) {
|
||||
QJsonObject metric = metricValue.toObject();
|
||||
QString name = metric[QStringLiteral("name")].toString();
|
||||
|
||||
if (name == QStringLiteral("resistance")) {
|
||||
lowerResistance = metric[QStringLiteral("lower")].toInt();
|
||||
upperResistance = metric[QStringLiteral("upper")].toInt();
|
||||
} else if (name == QStringLiteral("cadence")) {
|
||||
lowerCadence = metric[QStringLiteral("lower")].toInt();
|
||||
upperCadence = metric[QStringLiteral("upper")].toInt();
|
||||
} else if (name == QStringLiteral("power_zone")) {
|
||||
powerZoneFound = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if(powerZoneFound == true)
|
||||
break;
|
||||
|
||||
trainrow r;
|
||||
int duration = offsets[QStringLiteral("end")].toInt() - offsets[QStringLiteral("start")].toInt();
|
||||
if (i != 0) {
|
||||
// offsets have a 1s gap
|
||||
duration++;
|
||||
}
|
||||
|
||||
r.lower_requested_peloton_resistance = lowerResistance;
|
||||
r.upper_requested_peloton_resistance = upperResistance;
|
||||
r.lower_cadence = lowerCadence;
|
||||
r.upper_cadence = upperCadence;
|
||||
|
||||
r.average_requested_peloton_resistance =
|
||||
(r.lower_requested_peloton_resistance + r.upper_requested_peloton_resistance) / 2;
|
||||
r.average_cadence = (r.lower_cadence + r.upper_cadence) / 2;
|
||||
|
||||
if (bluetoothManager && bluetoothManager->device()) {
|
||||
if (bluetoothManager->device()->deviceType() == bluetoothdevice::BIKE) {
|
||||
r.lower_resistance = ((bike *)bluetoothManager->device())
|
||||
->pelotonToBikeResistance(lowerResistance);
|
||||
r.upper_resistance = ((bike *)bluetoothManager->device())
|
||||
->pelotonToBikeResistance(upperResistance);
|
||||
r.average_resistance = ((bike *)bluetoothManager->device())
|
||||
->pelotonToBikeResistance(r.average_requested_peloton_resistance);
|
||||
} else if (bluetoothManager->device()->deviceType() == bluetoothdevice::ELLIPTICAL) {
|
||||
r.lower_resistance =
|
||||
((elliptical *)bluetoothManager->device())
|
||||
->pelotonToEllipticalResistance(lowerResistance);
|
||||
r.upper_resistance =
|
||||
((elliptical *)bluetoothManager->device())
|
||||
->pelotonToEllipticalResistance(upperResistance);
|
||||
r.average_resistance = ((elliptical *)bluetoothManager->device())
|
||||
->pelotonToEllipticalResistance(r.average_requested_peloton_resistance);
|
||||
}
|
||||
}
|
||||
|
||||
// Set for compatibility
|
||||
if (difficulty == QStringLiteral("average")) {
|
||||
r.resistance = r.average_resistance;
|
||||
r.requested_peloton_resistance = r.average_requested_peloton_resistance;
|
||||
r.cadence = r.average_cadence;
|
||||
} else if (difficulty == QStringLiteral("upper")) {
|
||||
r.resistance = r.upper_resistance;
|
||||
r.requested_peloton_resistance = r.upper_requested_peloton_resistance;
|
||||
r.cadence = r.upper_cadence;
|
||||
} else { // lower
|
||||
r.resistance = r.lower_resistance;
|
||||
r.requested_peloton_resistance = r.lower_requested_peloton_resistance;
|
||||
r.cadence = r.lower_cadence;
|
||||
}
|
||||
|
||||
// Compact rows in the training program
|
||||
if (i == 0 ||
|
||||
(r.lower_requested_peloton_resistance != trainrows.last().lower_requested_peloton_resistance ||
|
||||
r.upper_requested_peloton_resistance != trainrows.last().upper_requested_peloton_resistance ||
|
||||
r.lower_cadence != trainrows.last().lower_cadence ||
|
||||
r.upper_cadence != trainrows.last().upper_cadence)) {
|
||||
r.duration = QTime(0, 0, 0).addSecs(duration);
|
||||
trainrows.append(r);
|
||||
} else {
|
||||
trainrows.last().duration = trainrows.last().duration.addSecs(duration);
|
||||
}
|
||||
}
|
||||
|
||||
foreach(trainrow r, trainrows) {
|
||||
qDebug() << r.duration << r.average_cadence << r.average_resistance;
|
||||
}
|
||||
|
||||
QJsonArray targetMetricsList = target_metrics_performance_data[QStringLiteral("target_metrics")].toArray();
|
||||
|
||||
bool atLeastOnePower = false;
|
||||
if (trainrows.empty() && !targetMetricsList.isEmpty() &&
|
||||
bluetoothManager->device()->deviceType() != bluetoothdevice::ROWING &&
|
||||
bluetoothManager->device()->deviceType() != bluetoothdevice::TREADMILL) {
|
||||
|
||||
for (QJsonValue metric : targetMetricsList) {
|
||||
QJsonObject metricObj = metric.toObject();
|
||||
QJsonObject offsets = metricObj[QStringLiteral("offsets")].toObject();
|
||||
int start = offsets[QStringLiteral("start")].toInt();
|
||||
int end = offsets[QStringLiteral("end")].toInt();
|
||||
int len = end - start;
|
||||
|
||||
QJsonArray metricsArray = metricObj[QStringLiteral("metrics")].toArray();
|
||||
if (!metricsArray.isEmpty()) {
|
||||
QJsonObject powerMetric = metricsArray[0].toObject();
|
||||
int zone = powerMetric[QStringLiteral("lower")].toInt();
|
||||
|
||||
trainrow r;
|
||||
r.duration = QTime(0, len / 60, len % 60, 0);
|
||||
|
||||
switch(zone) {
|
||||
case 1: // Zone 1 / Recovery
|
||||
r.power = settings.value(QZSettings::ftp, QZSettings::default_ftp).toDouble() * 0.50;
|
||||
break;
|
||||
case 2: // Zone 2
|
||||
r.power = settings.value(QZSettings::ftp, QZSettings::default_ftp).toDouble() * 0.66;
|
||||
break;
|
||||
case 3: // Zone 3
|
||||
r.power = settings.value(QZSettings::ftp, QZSettings::default_ftp).toDouble() * 0.83;
|
||||
break;
|
||||
case 4: // Zone 4 / Sweet Spot
|
||||
r.power = settings.value(QZSettings::ftp, QZSettings::default_ftp).toDouble() * 0.98;
|
||||
break;
|
||||
case 5: // Zone 5
|
||||
r.power = settings.value(QZSettings::ftp, QZSettings::default_ftp).toDouble() * 1.13;
|
||||
break;
|
||||
case 6: // Zone 6
|
||||
r.power = settings.value(QZSettings::ftp, QZSettings::default_ftp).toDouble() * 1.35;
|
||||
break;
|
||||
case 7: // Zone 7
|
||||
r.power = settings.value(QZSettings::ftp, QZSettings::default_ftp).toDouble() * 1.5;
|
||||
break;
|
||||
default:
|
||||
r.power = -1;
|
||||
break;
|
||||
}
|
||||
|
||||
if (r.power != -1) {
|
||||
atLeastOnePower = true;
|
||||
}
|
||||
trainrows.append(r);
|
||||
qDebug() << r.duration << "power" << r.power << "zone" << zone;
|
||||
}
|
||||
}
|
||||
|
||||
// If this list doesn't have anything useful for this session
|
||||
if (!atLeastOnePower) {
|
||||
trainrows.clear();
|
||||
}
|
||||
}
|
||||
} else if (!target_metrics_performance_data.isEmpty() && bluetoothManager->device() &&
|
||||
bluetoothManager->device()->deviceType() == bluetoothdevice::TREADMILL) {
|
||||
double miles = 1;
|
||||
bool treadmill_force_speed =
|
||||
@@ -1647,74 +1827,75 @@ double peloton::rowerpaceToSpeed(double pace) {
|
||||
}
|
||||
|
||||
void peloton::getInstructor(const QString &instructor_id) {
|
||||
QSettings settings;
|
||||
connect(mgr, &QNetworkAccessManager::finished, this, &peloton::instructor_onfinish);
|
||||
|
||||
QUrl url(QStringLiteral("https://api.onepeloton.com/api/instructor/") + instructor_id);
|
||||
QUrl url(QStringLiteral("https://api-3p.onepeloton.com/api/v1/instructor/") + instructor_id);
|
||||
qDebug() << "peloton::getInstructor" << url;
|
||||
QNetworkRequest request(url);
|
||||
|
||||
request.setHeader(QNetworkRequest::ContentTypeHeader, QStringLiteral("application/json"));
|
||||
request.setHeader(QNetworkRequest::UserAgentHeader, QStringLiteral("qdomyos-zwift"));
|
||||
RAWHEADER
|
||||
|
||||
mgr->get(request);
|
||||
}
|
||||
|
||||
void peloton::getRide(const QString &ride_id) {
|
||||
QSettings settings;
|
||||
connect(mgr, &QNetworkAccessManager::finished, this, &peloton::ride_onfinish);
|
||||
|
||||
QUrl url(QStringLiteral("https://api.onepeloton.com/api/ride/") + ride_id +
|
||||
QUrl url(QStringLiteral("https://api-3p.onepeloton.com/api/v1/ride/") + ride_id +
|
||||
QStringLiteral("/details?stream_source=multichannel"));
|
||||
qDebug() << "peloton::getRide" << url;
|
||||
QNetworkRequest request(url);
|
||||
|
||||
request.setHeader(QNetworkRequest::ContentTypeHeader, QStringLiteral("application/json"));
|
||||
request.setHeader(QNetworkRequest::UserAgentHeader, QStringLiteral("qdomyos-zwift"));
|
||||
RAWHEADER
|
||||
|
||||
mgr->get(request);
|
||||
}
|
||||
|
||||
void peloton::getPerformance(const QString &workout) {
|
||||
QSettings settings;
|
||||
connect(mgr, &QNetworkAccessManager::finished, this, &peloton::performance_onfinish);
|
||||
|
||||
QUrl url(QStringLiteral("https://api.onepeloton.com/api/workout/") + workout +
|
||||
QUrl url(QStringLiteral("https://api-3p.onepeloton.com/api/v1/workout/") + workout +
|
||||
QStringLiteral("/performance_graph?every_n=") + QString::number(peloton_workout_second_resolution));
|
||||
qDebug() << "peloton::getPerformance" << url;
|
||||
QNetworkRequest request(url);
|
||||
|
||||
request.setHeader(QNetworkRequest::ContentTypeHeader, QStringLiteral("application/json"));
|
||||
request.setHeader(QNetworkRequest::UserAgentHeader, QStringLiteral("qdomyos-zwift"));
|
||||
RAWHEADER
|
||||
|
||||
mgr->get(request);
|
||||
}
|
||||
|
||||
void peloton::getWorkout(const QString &workout) {
|
||||
QSettings settings;
|
||||
connect(mgr, &QNetworkAccessManager::finished, this, &peloton::workout_onfinish);
|
||||
|
||||
QUrl url(QStringLiteral("https://api.onepeloton.com/api/workout/") + workout);
|
||||
QUrl url(QStringLiteral("https://api-3p.onepeloton.com/api/v1/workout/") + workout);
|
||||
qDebug() << "peloton::getWorkout" << url;
|
||||
QNetworkRequest request(url);
|
||||
|
||||
request.setHeader(QNetworkRequest::ContentTypeHeader, QStringLiteral("application/json"));
|
||||
request.setHeader(QNetworkRequest::UserAgentHeader, QStringLiteral("qdomyos-zwift"));
|
||||
RAWHEADER
|
||||
|
||||
mgr->get(request);
|
||||
}
|
||||
|
||||
void peloton::getSummary(const QString &workout) {
|
||||
QSettings settings;
|
||||
connect(mgr, &QNetworkAccessManager::finished, this, &peloton::summary_onfinish);
|
||||
|
||||
QUrl url(QStringLiteral("https://api.onepeloton.com/api/workout/") + workout + QStringLiteral("/summary"));
|
||||
QUrl url(QStringLiteral("https://api-3p.onepeloton.com/api/v1/workout/") + workout + QStringLiteral("/summary"));
|
||||
qDebug() << "peloton::getSummary" << url;
|
||||
QNetworkRequest request(url);
|
||||
|
||||
request.setHeader(QNetworkRequest::ContentTypeHeader, QStringLiteral("application/json"));
|
||||
request.setHeader(QNetworkRequest::UserAgentHeader, QStringLiteral("qdomyos-zwift"));
|
||||
RAWHEADER
|
||||
|
||||
mgr->get(request);
|
||||
}
|
||||
|
||||
void peloton::getWorkoutList(int num) {
|
||||
Q_UNUSED(num)
|
||||
QSettings settings;
|
||||
// if (num == 0) { //NOTE: clang-analyzer-deadcode.DeadStores
|
||||
// num = this->total_workout;
|
||||
// }
|
||||
@@ -1727,16 +1908,277 @@ void peloton::getWorkoutList(int num) {
|
||||
|
||||
int current_page = 0;
|
||||
|
||||
QUrl url(QStringLiteral("https://api.onepeloton.com/api/user/") + user_id +
|
||||
QUrl url(QStringLiteral("https://api-3p.onepeloton.com/api/v1/user") +
|
||||
QStringLiteral("/workouts?sort_by=-created&page=") + QString::number(current_page) +
|
||||
QStringLiteral("&limit=") + QString::number(limit));
|
||||
qDebug() << "peloton::getWorkoutList" << url;
|
||||
QNetworkRequest request(url);
|
||||
|
||||
request.setHeader(QNetworkRequest::ContentTypeHeader, QStringLiteral("application/json"));
|
||||
request.setHeader(QNetworkRequest::UserAgentHeader, QStringLiteral("qdomyos-zwift"));
|
||||
RAWHEADER
|
||||
|
||||
mgr->get(request);
|
||||
}
|
||||
|
||||
void peloton::setTestMode(bool test) { testMode = test; }
|
||||
|
||||
void peloton::onPelotonGranted() {
|
||||
|
||||
pelotonAuthWebVisible = false;
|
||||
emit pelotonWebVisibleChanged(pelotonAuthWebVisible);
|
||||
QSettings settings;
|
||||
settings.setValue(QZSettings::peloton_accesstoken, pelotonOAuth->token());
|
||||
settings.setValue(QZSettings::peloton_refreshtoken, pelotonOAuth->refreshToken());
|
||||
settings.setValue(QZSettings::peloton_lastrefresh, QDateTime::currentDateTime());
|
||||
qDebug() << QStringLiteral("peloton authenticathed") << pelotonOAuth->token() << pelotonOAuth->refreshToken();
|
||||
peloton_refreshtoken();
|
||||
if(homeform::singleton())
|
||||
homeform::singleton()->setPelotonPopupVisible(true);
|
||||
}
|
||||
|
||||
void peloton::onPelotonAuthorizeWithBrowser(const QUrl &url) {
|
||||
|
||||
// ui->textBrowser->append(tr("Open with browser:") + url.toString());
|
||||
QSettings settings;
|
||||
bool strava_auth_external_webbrowser =
|
||||
settings.value(QZSettings::strava_auth_external_webbrowser, QZSettings::default_strava_auth_external_webbrowser)
|
||||
.toBool();
|
||||
#if defined(Q_OS_WIN) || (defined(Q_OS_MAC) && !defined(Q_OS_IOS))
|
||||
strava_auth_external_webbrowser = true;
|
||||
#endif
|
||||
pelotonAuthUrl = url.toString();
|
||||
emit pelotonAuthUrlChanged(pelotonAuthUrl);
|
||||
|
||||
if (strava_auth_external_webbrowser)
|
||||
QDesktopServices::openUrl(url);
|
||||
else {
|
||||
pelotonAuthWebVisible = true;
|
||||
emit pelotonWebVisibleChanged(pelotonAuthWebVisible);
|
||||
}
|
||||
}
|
||||
|
||||
void peloton::replyDataReceived(const QByteArray &v) {
|
||||
|
||||
qDebug() << v;
|
||||
|
||||
QByteArray data;
|
||||
QSettings settings;
|
||||
QString s(v);
|
||||
QJsonDocument jsonResponse = QJsonDocument::fromJson(s.toUtf8());
|
||||
settings.setValue(QZSettings::peloton_accesstoken, jsonResponse[QStringLiteral("access_token")]);
|
||||
settings.setValue(QZSettings::peloton_refreshtoken, jsonResponse[QStringLiteral("refresh_token")]);
|
||||
settings.setValue(QZSettings::peloton_expires, jsonResponse[QStringLiteral("expires_at")]);
|
||||
|
||||
qDebug() << jsonResponse[QStringLiteral("access_token")] << jsonResponse[QStringLiteral("refresh_token")]
|
||||
<< jsonResponse[QStringLiteral("expires_at")];
|
||||
|
||||
QString urlstr = QStringLiteral("https://www.peloton.com/oauth/token?");
|
||||
QUrlQuery params;
|
||||
params.addQueryItem(QStringLiteral("client_id"), QStringLiteral(PELOTON_CLIENT_ID_S));
|
||||
#ifdef PELOTON_SECRET_KEY
|
||||
#define _STR(x) #x
|
||||
#define STRINGIFY(x) _STR(x)
|
||||
params.addQueryItem("client_secret", STRINGIFY(PELOTON_SECRET_KEY));
|
||||
#endif
|
||||
|
||||
params.addQueryItem(QStringLiteral("code"), peloton_code);
|
||||
data.append(params.query(QUrl::FullyEncoded).toUtf8());
|
||||
|
||||
// trade-in the temporary access code retrieved by the Call-Back URL for the finale token
|
||||
QUrl url = QUrl(urlstr);
|
||||
|
||||
QNetworkRequest request = QNetworkRequest(url);
|
||||
request.setHeader(QNetworkRequest::ContentTypeHeader, QStringLiteral("application/x-www-form-urlencoded"));
|
||||
|
||||
// now get the final token - but ignore errors
|
||||
if (manager) {
|
||||
|
||||
delete manager;
|
||||
manager = 0;
|
||||
}
|
||||
manager = new QNetworkAccessManager(this);
|
||||
// connect(manager, SIGNAL(sslErrors(QNetworkReply*, const QList<QSslError> & )), this,
|
||||
// SLOT(onSslErrors(QNetworkReply*, const QList<QSslError> & ))); connect(manager,
|
||||
// SIGNAL(finished(QNetworkReply*)), this, SLOT(networkRequestFinished(QNetworkReply*)));
|
||||
manager->post(request, data);
|
||||
}
|
||||
|
||||
void peloton::onSslErrors(QNetworkReply *reply, const QList<QSslError> &error) {
|
||||
|
||||
reply->ignoreSslErrors();
|
||||
qDebug() << QStringLiteral("peloton::onSslErrors") << error;
|
||||
}
|
||||
|
||||
void peloton::networkRequestFinished(QNetworkReply *reply) {
|
||||
|
||||
QSettings settings;
|
||||
|
||||
// we can handle SSL handshake errors, if we got here then some kind of protocol was agreed
|
||||
if (reply->error() == QNetworkReply::NoError || reply->error() == QNetworkReply::SslHandshakeFailedError) {
|
||||
|
||||
QByteArray payload = reply->readAll(); // JSON
|
||||
QString refresh_token;
|
||||
QString access_token;
|
||||
|
||||
// parse the response and extract the tokens, pretty much the same for all services
|
||||
// although polar choose to also pass a user id, which is needed for future calls
|
||||
QJsonParseError parseError;
|
||||
QJsonDocument document = QJsonDocument::fromJson(payload, &parseError);
|
||||
if (parseError.error == QJsonParseError::NoError) {
|
||||
refresh_token = document[QStringLiteral("refresh_token")].toString();
|
||||
access_token = document[QStringLiteral("access_token")].toString();
|
||||
}
|
||||
|
||||
settings.setValue(QZSettings::peloton_accesstoken, access_token);
|
||||
settings.setValue(QZSettings::peloton_refreshtoken, refresh_token);
|
||||
settings.setValue(QZSettings::peloton_lastrefresh, QDateTime::currentDateTime());
|
||||
|
||||
qDebug() << access_token << refresh_token;
|
||||
|
||||
} else {
|
||||
|
||||
// general error getting response
|
||||
QString error =
|
||||
QString(tr("Error retrieving access token, %1 (%2)")).arg(reply->errorString()).arg(reply->error());
|
||||
qDebug() << error << reply->url() << reply->readAll();
|
||||
}
|
||||
}
|
||||
|
||||
void peloton::callbackReceived(const QVariantMap &values) {
|
||||
qDebug() << QStringLiteral("peloton::callbackReceived") << values;
|
||||
if (!values.value(QZSettings::peloton_code).toString().isEmpty()) {
|
||||
peloton_code = values.value(QZSettings::peloton_code).toString();
|
||||
|
||||
qDebug() << peloton_code;
|
||||
}
|
||||
}
|
||||
|
||||
QOAuth2AuthorizationCodeFlow *peloton::peloton_connect() {
|
||||
if (manager) {
|
||||
|
||||
delete manager;
|
||||
manager = nullptr;
|
||||
}
|
||||
if (pelotonOAuth) {
|
||||
|
||||
delete pelotonOAuth;
|
||||
pelotonOAuth = nullptr;
|
||||
}
|
||||
if (pelotonReplyHandler) {
|
||||
|
||||
delete pelotonReplyHandler;
|
||||
pelotonReplyHandler = nullptr;
|
||||
}
|
||||
manager = new QNetworkAccessManager(this);
|
||||
OAuth2Parameter parameter;
|
||||
pelotonOAuth = new QOAuth2AuthorizationCodeFlow(manager, this);
|
||||
pelotonOAuth->setScope(QStringLiteral("openid offline_access 3p.profile:r 3p.workout:r"));
|
||||
pelotonOAuth->setClientIdentifier(QStringLiteral(PELOTON_CLIENT_ID_S));
|
||||
pelotonOAuth->setAuthorizationUrl(QUrl(QStringLiteral("https://auth.onepeloton.com/oauth/authorize")));
|
||||
pelotonOAuth->setAccessTokenUrl(QUrl(QStringLiteral("https://auth.onepeloton.com/oauth/token")));
|
||||
pelotonOAuth->setModifyParametersFunction(
|
||||
buildModifyParametersFunction(QUrl(QLatin1String("")), QUrl(QLatin1String(""))));
|
||||
pelotonReplyHandler = new QOAuthHttpServerReplyHandler(QHostAddress(QStringLiteral("127.0.0.1")), 18080, this);
|
||||
connect(pelotonReplyHandler, &QOAuthHttpServerReplyHandler::replyDataReceived, this, &peloton::replyDataReceived);
|
||||
connect(pelotonReplyHandler, &QOAuthHttpServerReplyHandler::callbackReceived, this, &peloton::callbackReceived);
|
||||
|
||||
pelotonOAuth->setReplyHandler(pelotonReplyHandler);
|
||||
|
||||
return pelotonOAuth;
|
||||
}
|
||||
|
||||
void peloton::peloton_connect_clicked() {
|
||||
QLoggingCategory::setFilterRules(QStringLiteral("qt.networkauth.*=true"));
|
||||
|
||||
peloton_connect();
|
||||
connect(pelotonOAuth, &QOAuth2AuthorizationCodeFlow::authorizeWithBrowser, this, &peloton::onPelotonAuthorizeWithBrowser);
|
||||
connect(pelotonOAuth, &QOAuth2AuthorizationCodeFlow::granted, this, &peloton::onPelotonGranted);
|
||||
|
||||
pelotonOAuth->grant();
|
||||
// qDebug() <<
|
||||
// QAbstractOAuth2::post("https://www.peloton.com/oauth/authorize?client_id=7976&scope=activity:read_all,activity:write&redirect_uri=http://127.0.0.1&response_type=code&approval_prompt=force");
|
||||
}
|
||||
|
||||
QAbstractOAuth::ModifyParametersFunction peloton::buildModifyParametersFunction(const QUrl &clientIdentifier, const QUrl &clientIdentifierSharedKey) {
|
||||
return [clientIdentifier, clientIdentifierSharedKey](QAbstractOAuth::Stage stage, QVariantMap *parameters) {
|
||||
if (stage == QAbstractOAuth::Stage::RequestingAuthorization) {
|
||||
parameters->insert(QStringLiteral("audience"), QStringLiteral("https://api-3p.onepeloton.com/"));
|
||||
parameters->insert(QStringLiteral("responseType"), QStringLiteral("code")); /* Request refresh token*/
|
||||
parameters->insert(QStringLiteral("approval_prompt"),
|
||||
QStringLiteral("force")); /* force user check scope again */
|
||||
QByteArray code = parameters->value(QStringLiteral("code")).toByteArray();
|
||||
// DON'T TOUCH THIS LINE, THANKS Roberto Viola
|
||||
(*parameters)[QStringLiteral("code")] = QUrl::fromPercentEncoding(code); // NOTE: Old code replaced by
|
||||
}
|
||||
if (stage == QAbstractOAuth::Stage::RefreshingAccessToken) {
|
||||
parameters->insert(QStringLiteral("client_id"), clientIdentifier);
|
||||
parameters->insert(QStringLiteral("client_secret"), clientIdentifierSharedKey);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
void peloton::peloton_refreshtoken() {
|
||||
|
||||
QSettings settings;
|
||||
// QUrlQuery params; //NOTE: clazy-unuse-non-tirial-variable
|
||||
|
||||
if (settings.value(QZSettings::peloton_refreshtoken).toString().isEmpty()) {
|
||||
|
||||
peloton_connect();
|
||||
return;
|
||||
}
|
||||
|
||||
QNetworkRequest request(QUrl(QStringLiteral("https://auth.onepeloton.com/oauth/token?")));
|
||||
request.setRawHeader("Content-Type", "application/x-www-form-urlencoded");
|
||||
|
||||
// set params
|
||||
QString data;
|
||||
data += QStringLiteral("client_id=" PELOTON_CLIENT_ID_S);
|
||||
data += QStringLiteral("&refresh_token=") + settings.value(QZSettings::peloton_refreshtoken).toString();
|
||||
data += QStringLiteral("&grant_type=refresh_token");
|
||||
|
||||
// make request
|
||||
if (manager) {
|
||||
|
||||
delete manager;
|
||||
manager = nullptr;
|
||||
}
|
||||
manager = new QNetworkAccessManager(this);
|
||||
QNetworkReply *reply = manager->post(request, data.toLatin1());
|
||||
|
||||
// blocking request
|
||||
QEventLoop loop;
|
||||
connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit);
|
||||
loop.exec();
|
||||
|
||||
int statusCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
|
||||
qDebug() << QStringLiteral("HTTP response code: ") << statusCode;
|
||||
|
||||
// oops, no dice
|
||||
if (reply->error() != 0) {
|
||||
qDebug() << QStringLiteral("Got error") << reply->errorString().toStdString().c_str();
|
||||
homeform::singleton()->setToastRequested("Peloton Auth Failed!");
|
||||
return;
|
||||
}
|
||||
|
||||
// lets extract the access token, and possibly a new refresh token
|
||||
QByteArray r = reply->readAll();
|
||||
qDebug() << QStringLiteral("Got response:") << r.data();
|
||||
|
||||
QJsonParseError parseError;
|
||||
QJsonDocument document = QJsonDocument::fromJson(r, &parseError);
|
||||
|
||||
// failed to parse result !?
|
||||
if (parseError.error != QJsonParseError::NoError) {
|
||||
qDebug() << tr("JSON parser error") << parseError.errorString();
|
||||
}
|
||||
|
||||
QString access_token = document[QStringLiteral("access_token")].toString();
|
||||
QString refresh_token = document[QStringLiteral("refresh_token")].toString();
|
||||
|
||||
settings.setValue(QZSettings::peloton_accesstoken, access_token);
|
||||
settings.setValue(QZSettings::peloton_refreshtoken, refresh_token);
|
||||
settings.setValue(QZSettings::peloton_lastrefresh, QDateTime::currentDateTime());
|
||||
|
||||
homeform::singleton()->setToastRequested("Peloton Login OK!");
|
||||
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
#define PELOTON_H
|
||||
|
||||
#include "bluetooth.h"
|
||||
#include "OAuth2.h"
|
||||
#include "powerzonepack.h"
|
||||
#include "trainprogram.h"
|
||||
#include <QAbstractOAuth2>
|
||||
@@ -24,6 +25,14 @@
|
||||
#include "filedownloader.h"
|
||||
#include "homefitnessbuddy.h"
|
||||
|
||||
#if defined(WIN32)
|
||||
#pragma message("DEFINE PELOTON_SECRET_KEY!!!")
|
||||
#else
|
||||
#warning "DEFINE PELOTON_SECRET_KEY!!!"
|
||||
#endif
|
||||
|
||||
#define PELOTON_CLIENT_ID_S STRINGIFY(PELOTON_SECRET_KEY)
|
||||
|
||||
class peloton : public QObject {
|
||||
|
||||
Q_OBJECT
|
||||
@@ -50,6 +59,10 @@ class peloton : public QObject {
|
||||
int current_pedaling_duration = 0;
|
||||
qint64 start_time = 0;
|
||||
|
||||
// OAuth
|
||||
QString pelotonAuthUrl;
|
||||
bool pelotonAuthWebVisible;
|
||||
|
||||
void setTestMode(bool test);
|
||||
|
||||
bool isWorkoutInProgress() {
|
||||
@@ -84,6 +97,17 @@ class peloton : public QObject {
|
||||
|
||||
bool testMode = false;
|
||||
|
||||
//OAuth
|
||||
QOAuth2AuthorizationCodeFlow *pelotonOAuth = nullptr;
|
||||
QNetworkAccessManager *manager = nullptr;
|
||||
QOAuthHttpServerReplyHandler *pelotonReplyHandler = nullptr;
|
||||
QString peloton_code;
|
||||
QOAuth2AuthorizationCodeFlow *peloton_connect();
|
||||
void peloton_refreshtoken();
|
||||
QNetworkReply *replyPeloton;
|
||||
QAbstractOAuth::ModifyParametersFunction buildModifyParametersFunction(const QUrl &clientIdentifier,
|
||||
const QUrl &clientIdentifierSharedKey);
|
||||
|
||||
// rowers
|
||||
double rowerpaceToSpeed(double pace);
|
||||
typedef struct _peloton_rower_pace_intensities_level {
|
||||
@@ -118,6 +142,9 @@ class peloton : public QObject {
|
||||
|
||||
_peloton_treadmill_pace_intensities treadmill_pace[7];
|
||||
|
||||
public slots:
|
||||
void peloton_connect_clicked();
|
||||
|
||||
private slots:
|
||||
void login_onfinish(QNetworkReply *reply);
|
||||
void workoutlist_onfinish(QNetworkReply *reply);
|
||||
@@ -130,6 +157,14 @@ class peloton : public QObject {
|
||||
void hfb_trainrows(QList<trainrow> *list);
|
||||
void pzp_loginState(bool ok);
|
||||
|
||||
// OAuth
|
||||
void onPelotonGranted();
|
||||
void onPelotonAuthorizeWithBrowser(const QUrl &url);
|
||||
void replyDataReceived(const QByteArray &v);
|
||||
void onSslErrors(QNetworkReply *reply, const QList<QSslError> &error);
|
||||
void networkRequestFinished(QNetworkReply *reply);
|
||||
void callbackReceived(const QVariantMap &values);
|
||||
|
||||
void startEngine();
|
||||
|
||||
signals:
|
||||
@@ -137,6 +172,8 @@ class peloton : public QObject {
|
||||
void pzpLoginState(bool ok);
|
||||
void workoutStarted(QString name, QString instructor);
|
||||
void workoutChanged(QString name, QString instructor);
|
||||
void pelotonAuthUrlChanged(QString value);
|
||||
void pelotonWebVisibleChanged(bool value);
|
||||
};
|
||||
|
||||
#endif // PELOTON_H
|
||||
|
||||
@@ -331,6 +331,7 @@ INCLUDEPATH += fit-sdk/ devices/
|
||||
|
||||
HEADERS += \
|
||||
$$PWD/EventHandler.h \
|
||||
$$PWD/OAuth2.h \
|
||||
$$PWD/devices/antbike/antbike.h \
|
||||
$$PWD/devices/crossrope/crossrope.h \
|
||||
$$PWD/devices/cycleopsphantombike/cycleopsphantombike.h \
|
||||
|
||||
@@ -114,5 +114,6 @@
|
||||
<file>StaticAccordionElement.qml</file>
|
||||
<file>inner_templates/chartjs/dotreadmillchartlive.js</file>
|
||||
<file>inner_templates/chartjs/treadmillchartlive.htm</file>
|
||||
<file>WebPelotonAuth.qml</file>
|
||||
</qresource>
|
||||
</RCC>
|
||||
|
||||
@@ -764,6 +764,16 @@ const QString QZSettings::zwiftplay_swap = QStringLiteral("zwiftplay_swap");
|
||||
const QString QZSettings::gears_zwift_ratio = QStringLiteral("gears_zwift_ratio");
|
||||
const QString QZSettings::domyos_bike_500_profile_v2 = QStringLiteral("domyos_bike_500_profile_v2");
|
||||
const QString QZSettings::gears_offset = QStringLiteral("gears_offset");
|
||||
const QString QZSettings::peloton_accesstoken = QStringLiteral("peloton_accesstoken");
|
||||
const QString QZSettings::default_peloton_accesstoken = QStringLiteral("");
|
||||
const QString QZSettings::peloton_refreshtoken = QStringLiteral("peloton_refreshtoken");
|
||||
const QString QZSettings::default_peloton_refreshtoken = QStringLiteral("");
|
||||
const QString QZSettings::peloton_lastrefresh = QStringLiteral("peloton_lastrefresh");
|
||||
const QString QZSettings::default_peloton_lastrefresh = QStringLiteral("");
|
||||
const QString QZSettings::peloton_expires = QStringLiteral("peloton_expires");
|
||||
const QString QZSettings::default_peloton_expires = QStringLiteral("");
|
||||
const QString QZSettings::peloton_code = QStringLiteral("peloton_code");
|
||||
const QString QZSettings::default_peloton_code = QStringLiteral("");
|
||||
const QString QZSettings::proform_carbon_tl_PFTL59720 = QStringLiteral("proform_carbon_tl_PFTL59720");
|
||||
const QString QZSettings::proform_treadmill_sport_70 = QStringLiteral("proform_treadmill_sport_70");
|
||||
const QString QZSettings::peloton_date_format = QStringLiteral("peloton_date_format");
|
||||
@@ -814,7 +824,7 @@ const QString QZSettings::treadmill_follow_wattage = QStringLiteral("treadmill_f
|
||||
const QString QZSettings::fit_file_garmin_device_training_effect = QStringLiteral("fit_file_garmin_device_training_effect");
|
||||
const QString QZSettings::proform_treadmill_705_cst_V80_44 = QStringLiteral("proform_treadmill_705_cst_V80_44");
|
||||
|
||||
const uint32_t allSettingsCount = 687;
|
||||
const uint32_t allSettingsCount = 692;
|
||||
|
||||
QVariant allSettings[allSettingsCount][2] = {
|
||||
{QZSettings::cryptoKeySettingsProfiles, QZSettings::default_cryptoKeySettingsProfiles},
|
||||
@@ -1467,6 +1477,11 @@ QVariant allSettings[allSettingsCount][2] = {
|
||||
{QZSettings::gears_zwift_ratio, QZSettings::default_gears_zwift_ratio},
|
||||
{QZSettings::domyos_bike_500_profile_v2, QZSettings::default_domyos_bike_500_profile_v2},
|
||||
{QZSettings::gears_offset, QZSettings::default_gears_offset},
|
||||
{QZSettings::peloton_accesstoken, QZSettings::default_peloton_accesstoken},
|
||||
{QZSettings::peloton_refreshtoken, QZSettings::default_peloton_refreshtoken},
|
||||
{QZSettings::peloton_lastrefresh, QZSettings::default_peloton_lastrefresh},
|
||||
{QZSettings::peloton_expires, QZSettings::default_peloton_expires},
|
||||
{QZSettings::peloton_code, QZSettings::default_peloton_code},
|
||||
{QZSettings::proform_carbon_tl_PFTL59720, QZSettings::default_proform_carbon_tl_PFTL59720},
|
||||
{QZSettings::proform_treadmill_sport_70, QZSettings::default_proform_treadmill_sport_70},
|
||||
{QZSettings::peloton_date_format, QZSettings::default_peloton_date_format},
|
||||
|
||||
@@ -2138,6 +2138,21 @@ class QZSettings {
|
||||
static const QString gears_offset;
|
||||
static constexpr double default_gears_offset = 0.0;
|
||||
|
||||
static const QString peloton_accesstoken;
|
||||
static const QString default_peloton_accesstoken;
|
||||
|
||||
static const QString peloton_refreshtoken;
|
||||
static const QString default_peloton_refreshtoken;
|
||||
|
||||
static const QString peloton_lastrefresh;
|
||||
static const QString default_peloton_lastrefresh;
|
||||
|
||||
static const QString peloton_expires;
|
||||
static const QString default_peloton_expires;
|
||||
|
||||
static const QString peloton_code;
|
||||
static const QString default_peloton_code;
|
||||
|
||||
static const QString proform_carbon_tl_PFTL59720;
|
||||
static constexpr bool default_proform_carbon_tl_PFTL59720 = false;
|
||||
|
||||
|
||||
@@ -15,6 +15,8 @@ import QtQuick.Dialogs 1.0
|
||||
//anchors.bottomMargin: footerSettings.height + 10
|
||||
id: settingsPane
|
||||
|
||||
signal peloton_connect_clicked()
|
||||
|
||||
Settings {
|
||||
id: settings
|
||||
property real ui_zoom: 100.0
|
||||
@@ -977,6 +979,7 @@ import QtQuick.Dialogs 1.0
|
||||
property bool gears_zwift_ratio: false
|
||||
property bool domyos_bike_500_profile_v2: false
|
||||
property double gears_offset: 0.0
|
||||
|
||||
property bool proform_carbon_tl_PFTL59720: false
|
||||
|
||||
// from version 2.16.71
|
||||
@@ -4184,7 +4187,7 @@ import QtQuick.Dialogs 1.0
|
||||
color: Material.backgroundColor
|
||||
accordionContent: ColumnLayout {
|
||||
spacing: 0
|
||||
|
||||
/*
|
||||
RowLayout {
|
||||
spacing: 10
|
||||
Label {
|
||||
@@ -4261,6 +4264,23 @@ import QtQuick.Dialogs 1.0
|
||||
Layout.fillWidth: true
|
||||
color: Material.color(Material.Lime)
|
||||
}
|
||||
*/
|
||||
|
||||
ItemDelegate {
|
||||
Image {
|
||||
anchors.left: parent.left;
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
source: "icons/icons/Button_Connect_Rect_DarkMode.png"
|
||||
fillMode: Image.PreserveAspectFit
|
||||
visible: true
|
||||
width: parent.width
|
||||
}
|
||||
Layout.fillWidth: true
|
||||
onClicked: {
|
||||
stackView.push("WebPelotonAuth.qml")
|
||||
peloton_connect_clicked()
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
spacing: 10
|
||||
|
||||
Reference in New Issue
Block a user