Compare commits

...

51 Commits

Author SHA1 Message Date
Roberto Viola
1a1fe9ca1c Update project.pbxproj 2025-02-11 15:07:39 +01:00
Roberto Viola
3088168824 Merge branch 'master' into peloton_oauth 2025-02-11 15:07:10 +01:00
Roberto Viola
9483b4ffed Merge branch 'master' into peloton_oauth 2025-02-11 09:51:43 +01:00
Roberto Viola
8a04a242fd getting wattage and cadence directly from the zwift hub riding data if available 2025-02-11 09:51:06 +01:00
Roberto Viola
d5455aeefc Update project.pbxproj 2025-02-09 18:31:59 +01:00
Roberto Viola
104f85b949 Merge branch 'master' into peloton_oauth 2025-02-09 18:31:34 +01:00
Roberto Viola
0e886e0954 Update project.pbxproj 2025-02-08 18:04:38 +01:00
Roberto Viola
affefa36d7 Merge branch 'master' into peloton_oauth 2025-02-08 17:58:15 +01:00
Roberto Viola
44a882e56e fixing spinups in powerzone classes 2025-02-06 17:39:10 +01:00
Roberto Viola
043526e6fc airdate and current_pedaling_duration fixed 2025-02-06 10:13:56 +01:00
Roberto Viola
f5b6734a46 Update qzsettings.cpp 2025-02-06 10:06:07 +01:00
Roberto Viola
18a86474b7 Merge branch 'master' into peloton_oauth 2025-02-06 09:59:16 +01:00
Roberto Viola
46cbe877d9 Nordictrack commercial 1750 incline calibration incorrect (Issue #3118) 2025-02-04 09:28:06 +01:00
Roberto Viola
84fb1fe33b CycleOps Phantom 5 (Issue #3004) 2025-02-04 08:29:16 +01:00
Roberto Viola
4a98a7e694 Cannot set Virtufit Etappe 2 auto resistance (Issue #3130) 2025-02-04 08:23:08 +01:00
Roberto Viola
03b250856a new kingsmith variant treadmill 2025-02-04 07:37:45 +01:00
Roberto Viola
66513305f5 Update project.pbxproj 2025-02-04 07:35:22 +01:00
Roberto Viola
8245d76924 added peloton button on the settings page too 2025-02-03 09:20:14 +01:00
Roberto Viola
b2623a9439 Merge branch 'master' into peloton_oauth 2025-02-03 09:08:47 +01:00
Roberto Viola
a522ded028 everything should be fine now 2025-01-31 16:34:08 +01:00
Roberto Viola
b905e61321 Update main.qml 2025-01-31 16:13:00 +01:00
Roberto Viola
e1c237a781 fixing peloton popup 2025-01-31 16:12:11 +01:00
Roberto Viola
3a7b205ebc Merge branch 'master' into peloton_oauth 2025-01-31 10:54:31 +01:00
Roberto Viola
7d23a95355 adding popup to switch to the new api and removed credentials from the settings 2025-01-13 12:18:24 +01:00
Roberto Viola
a283189de3 Merge branch 'master' into peloton_oauth 2025-01-13 12:08:09 +01:00
Roberto Viola
4641a1174e adding the peloton connect button 2025-01-10 16:19:20 +01:00
Roberto Viola
5382016c1f Merge branch 'master' into peloton_oauth 2025-01-10 15:43:25 +01:00
Roberto Viola
4d75fe4716 Update qzsettings.cpp 2024-10-24 08:23:56 +02:00
Roberto Viola
94e60a934b Merge branch 'master' into peloton_oauth 2024-10-24 08:22:50 +02:00
Roberto Viola
27de1ab726 Update peloton.cpp 2024-10-04 12:01:15 +02:00
Roberto Viola
4eca29b090 Merge branch 'master' into peloton_oauth 2024-10-04 11:14:17 +02:00
Roberto Viola
d6ea58be55 Update peloton.cpp 2024-10-03 10:43:45 +02:00
Roberto Viola
560e718119 Update peloton.cpp 2024-10-03 10:14:29 +02:00
Roberto Viola
0a4784396c handling new cases 2024-10-03 10:13:19 +02:00
Roberto Viola
7023ef3f4c Update peloton.cpp 2024-10-02 21:03:15 +02:00
Roberto Viola
58ea93252a fixing auth header on workout 2024-10-01 07:36:30 +02:00
Roberto Viola
0d209bacd3 Merge branch 'master' into peloton_oauth 2024-10-01 07:23:52 +02:00
Roberto Viola
0abf84d57c Merge branch 'master' into peloton_oauth 2024-09-30 11:57:17 +02:00
Roberto Viola
9eda2c932e Merge branch 'master' into peloton_oauth 2024-09-27 14:15:45 +02:00
Roberto Viola
299139cdac workout api returns always void 2024-09-26 09:56:29 +02:00
Roberto Viola
7266e57ee0 Merge branch 'master' into peloton_oauth 2024-09-26 09:37:12 +02:00
Roberto Viola
2eafe4b17a Update peloton.cpp 2024-09-25 09:17:56 +02:00
Roberto Viola
255d657075 Update peloton.cpp 2024-09-24 12:29:49 +02:00
Roberto Viola
afc18c8670 Update peloton.cpp 2024-09-21 08:08:47 +02:00
Roberto Viola
618432428a trying to login 2024-09-20 14:58:38 +02:00
Roberto Viola
6e3b7e94f6 Update peloton.cpp 2024-09-17 18:09:09 +02:00
Roberto Viola
eafe480ab6 Update peloton.cpp 2024-09-17 12:27:55 +02:00
Roberto Viola
86ae28e15e Update settings.qml 2024-09-17 12:18:20 +02:00
Roberto Viola
86f5c22fe2 Update peloton.cpp 2024-09-17 12:11:25 +02:00
Roberto Viola
38f4bc6fda builds? 2024-09-17 11:58:12 +02:00
Roberto Viola
483624cd3b starting 2024-09-17 10:15:20 +02:00
15 changed files with 789 additions and 67 deletions

View File

@@ -4233,7 +4233,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_ENTITLEMENTS = "../src/ios/qdomyos-zwift.entitlements";
COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 1023;
CURRENT_PROJECT_VERSION = 1024;
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 = 1023;
CURRENT_PROJECT_VERSION = 1024;
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 = 1023;
CURRENT_PROJECT_VERSION = 1024;
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 = 1023;
CURRENT_PROJECT_VERSION = 1024;
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 = 1023;
CURRENT_PROJECT_VERSION = 1024;
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 = 1023;
CURRENT_PROJECT_VERSION = 1024;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_ASSET_PATHS = "\"watchkit Extension/Preview Content\"";
ENABLE_BITCODE = YES;

26
src/OAuth2.h Normal file
View 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
View 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!")
}
}
}
}

View File

@@ -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,
@@ -591,6 +589,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/");
@@ -6363,22 +6364,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) {
@@ -6815,6 +6800,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) {

View File

@@ -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();
@@ -704,6 +713,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;
@@ -905,6 +915,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);
@@ -930,6 +941,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);

View File

@@ -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>

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

View File

@@ -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"

View File

@@ -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);
@@ -805,12 +817,12 @@ void peloton::workout_onfinish(QNetworkReply *reply) {
current_instructor_id = ride[QStringLiteral("instructor_id")].toString();
current_ride_id = ride[QStringLiteral("id")].toString();
current_workout_type = ride[QStringLiteral("fitness_discipline")].toString();
current_pedaling_duration = ride[QStringLiteral("pedaling_duration")].toInt();
current_pedaling_duration = ride[QStringLiteral("duration")].toInt();
current_image_url = ride[QStringLiteral("image_url")].toString();
qint64 time = ride[QStringLiteral("original_air_time")].toInt();
qDebug() << QStringLiteral("original_air_time") << time;
qDebug() << QStringLiteral("current_pedaling_duration") << current_pedaling_duration;
qint64 time = ride[QStringLiteral("scheduled_start_time")].toInt();
qDebug() << QStringLiteral("scheduled_start_time") << time;
qDebug() << QStringLiteral("duration") << current_pedaling_duration;
current_original_air_time = QDateTime::fromSecsSinceEpoch(time, Qt::UTC);
@@ -1391,7 +1403,191 @@ 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) {
int lastEnd = 60;
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 + 1;
// Check if there's a gap from previous segment
if (!trainrows.isEmpty()) {
int prevEnd = start - 1; // Expected previous end
if (lastEnd < prevEnd) {
// Add gap row
trainrow gapRow;
gapRow.duration = QTime(0, (prevEnd - lastEnd + 1) / 60, (prevEnd - lastEnd + 1) % 60, 0);
gapRow.power = -1;
qDebug() << "adding a gap row of " << gapRow.duration << " seconds because start was " << start << " and end " << lastEnd;
trainrows.append(gapRow);
}
}
lastEnd = end;
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 +1843,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 +1924,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!");
}

View File

@@ -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() {
@@ -58,7 +71,7 @@ class peloton : public QObject {
private:
_PELOTON_API current_api = peloton_api;
const int peloton_workout_second_resolution = 10;
const int peloton_workout_second_resolution = 1;
bool peloton_credentials_wrong = false;
QNetworkAccessManager *mgr = nullptr;
@@ -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

View File

@@ -334,6 +334,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 \

View File

@@ -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>

View File

@@ -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");
@@ -875,7 +885,7 @@ const QString QZSettings::proform_bike_PFEVEX71316_0 = QStringLiteral("proform_b
const QString QZSettings::real_inclination_to_virtual_treamill_bridge = QStringLiteral("real_inclination_to_virtual_treamill_bridge");
const uint32_t allSettingsCount = 725;
const uint32_t allSettingsCount = 730;
QVariant allSettings[allSettingsCount][2] = {
{QZSettings::cryptoKeySettingsProfiles, QZSettings::default_cryptoKeySettingsProfiles},
@@ -1528,6 +1538,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},

View File

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

View File

@@ -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
@@ -4251,7 +4254,7 @@ import QtQuick.Dialogs 1.0
color: Material.backgroundColor
accordionContent: ColumnLayout {
spacing: 0
/*
RowLayout {
spacing: 10
Label {
@@ -4328,6 +4331,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