added oauth

master
nymea 2019-08-13 09:45:22 +02:00 committed by Michael Zanetti
parent d4c0bd41c8
commit 85060d152c
8 changed files with 388 additions and 164 deletions

View File

@ -41,85 +41,44 @@ DevicePluginSonos::~DevicePluginSonos()
Device::DeviceSetupStatus DevicePluginSonos::setupDevice(Device *device)
{
if (!sonos) {
if (!m_pluginTimer) {
}
if (device->deviceClassId() == sonosConnectionDeviceClassId) {
Sonos *sonos = new Sonos("0a8f6d44-d9d1-4474-bcfa-cfb41f8b66e8", this);
pluginStorage()->beginGroup(device->id().toString());
QString username = pluginStorage()->value("username").toString();
QString password = pluginStorage()->value("password").toString();
pluginStorage()->endGroup();
sonos->authenticate(username, password);
m_sonosConnections.insert(device->id(), sonos);
connect(sonos, &Sonos::authenticationFinished, this, [this, sonos](bool success){
if (success) {
} else {
qCWarning(dcSonos()) << "Cannot authenticate to Sonos api";
}
});
}
if (device->deviceClassId() == sonosDeviceClassId) {
if (!m_sonosSystem->Discover()) {
return Device::DeviceSetupStatusFailure;
}
QString zoneName = device->paramValue(sonosDeviceZoneNameParamTypeId).toString();
if (device->deviceClassId() == sonosGroupDeviceClassId) {
SONOS::ZoneList zones = m_sonosSystem->GetZoneList();
for(SONOS::ZoneList::const_iterator iz = zones.begin(); iz != zones.end(); ++iz) {
if (iz->second->GetZoneName().c_str() == zoneName) {
if (!m_sonosSystem->ConnectZone(iz->second, nullptr, nullptr)) {
qCDebug(dcSonos()) << "Failed connecting to zone" << zoneName;
return Device::DeviceSetupStatusFailure;
}
}
}
device->setStateValue(sonosConnectedStateTypeId, true);
//set parent ID
}
return Device::DeviceSetupStatusSuccess;
}
void DevicePluginSonos::postSetupDevice(Device *device)
{
if (device->deviceClassId() == sonosDeviceClassId) {
SONOS::ZonePtr pl = m_sonosSystem->GetConnectedZone();
uint8_t volume;
uint8_t mute;
for (SONOS::Zone::iterator ip = pl->begin(); ip != pl->end(); ++ip) {
if (!m_sonosSystem->GetPlayer()->GetVolume((*ip)->GetUUID(), &volume)) {
qWarning(dcSonos()) << "Could not get volume for" << (*ip)->GetHost().c_str();
} else {
device->setStateValue(sonosVolumeStateTypeId, volume);
}
if (device->deviceClassId() == sonosConnectionDeviceClassId) {
if (!m_sonosSystem->GetPlayer()->GetMute((*ip)->GetUUID(), &mute)) {
qWarning(dcSonos()) << "Could not get mute state for" << (*ip)->GetHost().c_str();
} else {
device->setStateValue(sonosMuteStateTypeId, mute);
}
}
}
while(m_sonosSystem->GetPlayer()->TransportPropertyEmpty());
if (device->deviceClassId() == sonosGroupDeviceClassId) {
SONOS::AVTProperty properties = m_sonosSystem->GetPlayer()->GetTransportProperty();
qDebug(dcSonos()) << "Transport Status" << properties.TransportStatus.c_str();
qDebug(dcSonos()) << "Transport State" << properties.TransportState.c_str();
if (QString(properties.TransportState.c_str()) == "PLAYING") {
device->setStateValue(sonosPlaybackStatusStateTypeId, "Playing");
} else if (QString(properties.TransportState.c_str()) == "PAUSED") {
device->setStateValue(sonosPlaybackStatusStateTypeId, "Paused");
} else if (QString(properties.TransportState.c_str()) == "STOPPED") {
device->setStateValue(sonosPlaybackStatusStateTypeId, "Stopped");
}
qDebug(dcSonos()) << "AVTransport URI" << properties.AVTransportURI.c_str();
qDebug(dcSonos()) << "AVTransportTitle" << properties.AVTransportURIMetaData->GetValue("dc:title").c_str();
qDebug(dcSonos()) << "Current Track" << properties.CurrentTrack;
qDebug(dcSonos()) << "Current Track Duration" << properties.CurrentTrackDuration.c_str();
qDebug(dcSonos()) << "Current Track URI" << properties.CurrentTrackURI.c_str();
//device->setStateValue(sonosArtworkStateTypeId, properties.CurrentTrackURI.c_str());
qDebug(dcSonos()) << "Current Track Title" << properties.CurrentTrackMetaData->GetValue("dc:title").c_str();
device->setStateValue(sonosTitleStateTypeId, properties.CurrentTrackMetaData->GetValue("dc:title").c_str());
qDebug(dcSonos()) << "Current Track Album" << properties.CurrentTrackMetaData->GetValue("upnp:album").c_str();
qDebug(dcSonos()) << "Current Track Artist" << properties.CurrentTrackMetaData->GetValue("dc:creator").c_str();
device->setStateValue(sonosArtistStateTypeId, properties.CurrentTrackMetaData->GetValue("dc:creator").c_str());
qDebug(dcSonos()) << "Current Crossfade Mode" << properties.CurrentCrossfadeMode.c_str();
qDebug(dcSonos()) << "Current Play Mode" << properties.CurrentPlayMode.c_str();
qDebug(dcSonos()) << "Current TransportActions" << properties.CurrentTransportActions.c_str();
qDebug(dcSonos()) << "Number Of Tracks" << properties.NumberOfTracks;
qDebug(dcSonos()) << "Alarm Running" << properties.r_AlarmRunning.c_str();
qDebug(dcSonos()) << "Alarm ID Running" << properties.r_AlarmIDRunning.c_str();
qDebug(dcSonos()) << "Alarm Logged Start Time" << properties.r_AlarmLoggedStartTime.c_str();
qDebug(dcSonos()) << "AlarmState" << properties.r_AlarmState.c_str();
}
}
@ -127,100 +86,59 @@ void DevicePluginSonos::deviceRemoved(Device *device)
{
qCDebug(dcSonos) << "Delete " << device->name();
if (myDevices().empty()) {
delete m_sonosSystem;
}
}
Device::DeviceError DevicePluginSonos::discoverDevices(const DeviceClassId &deviceClassId, const ParamList &params)
{
Q_UNUSED(params)
Q_UNUSED(deviceClassId)
if (!m_sonosSystem->Discover()) {
return Device::DeviceErrorDeviceNotFound;
}
QList<DeviceDescriptor> descriptors;
SONOS::ZoneList zones = m_sonosSystem->GetZoneList();
for (SONOS::ZoneList::const_iterator it = zones.begin(); it != zones.end(); ++it) {
qDebug(dcSonos()) << "Found zone" << it->second->GetZoneName().c_str();
DeviceDescriptor descriptor(sonosDeviceClassId, it->second->GetZoneName().c_str());
ParamList params;
params << Param(sonosDeviceZoneNameParamTypeId, it->second->GetZoneName().c_str());
descriptor.setParams(params);
descriptors << descriptor;
}
emit devicesDiscovered(sonosDeviceClassId, descriptors);
return Device::DeviceErrorAsync;
}
Device::DeviceError DevicePluginSonos::executeAction(Device *device, const Action &action)
{
Q_UNUSED(action)
if (device->deviceClassId() == sonosDeviceClassId) {
if (device->deviceClassId() == sonosGroupDeviceClassId) {
Sonos *sonos = m_sonosConnections.value(device->parentId());
int groupId = device->paramValue(sonosGroupDe)
if (!sonos)
return Device::DeviceErrorInvalidParameter;
if (action.actionTypeId() == sonosPlayActionTypeId) {
if (!m_sonosSystem->GetPlayer()->Play()) {
return Device::DeviceErrorHardwareFailure;
}
return Device::DeviceErrorNoError;
if (action.actionTypeId() == sonosGroupPlayActionTypeId) {
sonos->play();
return Device::DeviceErrorAsync;
}
if (action.actionTypeId() == sonosShuffleActionTypeId) {
SONOS::PlayMode_t mode = SONOS::PlayMode_t::PlayMode_NORMAL;;
if (action.param(sonosShuffleActionShuffleParamTypeId).value().toBool()) {
mode = SONOS::PlayMode_t::PlayMode_NORMAL;
}
if (!m_sonosSystem->GetPlayer()->SetPlayMode(mode)) {
return Device::DeviceErrorHardwareFailure;
}
return Device::DeviceErrorNoError;
if (action.actionTypeId() == sonosGroupShuffleActionTypeId) {
return Device::DeviceErrorAsync;
}
if (action.actionTypeId() == sonosRepeatActionTypeId) {
SONOS::PlayMode_t mode;
if (action.param(sonosRepeatActionRepeatParamTypeId).value().toString() == "None") {
mode = SONOS::PlayMode_t::PlayMode_NORMAL;
} else if (action.param(sonosShuffleActionShuffleParamTypeId).value().toString() == "One") {
mode = SONOS::PlayMode_t::PlayMode_REPEAT_ONE;
} else if (action.param(sonosShuffleActionShuffleParamTypeId).value().toString() == "All") {
mode = SONOS::PlayMode_t::PlayMode_REPEAT_ALL;
if (action.actionTypeId() == sonosGroupRepeatActionTypeId) {
if (action.param(sonosGroupRepeatActionRepeatParamTypeId).value().toString() == "None") {
} else if (action.param(sonosGroupShuffleActionShuffleParamTypeId).value().toString() == "One") {
} else if (action.param(sonosGroupShuffleActionShuffleParamTypeId).value().toString() == "All") {
} else {
return Device::DeviceErrorHardwareFailure;
}
if (!m_sonosSystem->GetPlayer()->SetPlayMode(mode)) {
return Device::DeviceErrorHardwareFailure;
}
return Device::DeviceErrorAsync;
}
if (action.actionTypeId() == sonosGroupPauseActionTypeId) {
sonos->pause();
return Device::DeviceErrorNoError;
}
if (action.actionTypeId() == sonosPauseActionTypeId) {
if (!m_sonosSystem->GetPlayer()->Pause()) {
return Device::DeviceErrorHardwareFailure;
}
if (action.actionTypeId() == sonosGroupStopActionTypeId) {
sonos->stop();
return Device::DeviceErrorNoError;
}
if (action.actionTypeId() == sonosStopActionTypeId) {
if (!m_sonosSystem->GetPlayer()->Stop()) {
return Device::DeviceErrorHardwareFailure;
}
return Device::DeviceErrorNoError;
}
if (action.actionTypeId() == sonosGroupMuteActionTypeId) {
bool mute = action.param(sonosGroupMuteActionMuteParamTypeId).value().toBool();
if (action.actionTypeId() == sonosMuteActionTypeId) {
bool mute = action.param(sonosMuteActionMuteParamTypeId).value().toBool();
SONOS::ZonePtr pl = m_sonosSystem->GetConnectedZone();
for (SONOS::Zone::iterator ip = pl->begin(); ip != pl->end(); ++ip) {
if (!m_sonosSystem->GetPlayer()->SetMute((*ip)->GetUUID(), mute)) {
qWarning(dcSonos()) << "Could not set mute state for" << (*ip)->GetHost().c_str();
return Device::DeviceErrorHardwareFailure;
}
}
sonos->setGroupMute()
return Device::DeviceErrorNoError;
}

View File

@ -48,7 +48,9 @@ public:
Device::DeviceError executeAction(Device *device, const Action &action) override;
private:
Sonos *sonos = nullptr;
PluginTimer *m_pluginTimer = nullptr;
QHash<DeviceId, Sonos *> m_sonosConnections;
private slots:
void onPluginTimer();

View File

@ -11,12 +11,19 @@
"deviceClasses": [
{
"id": "22df416d-7732-44f1-b6b9-e41296211178",
"name": "sonosGroup",
"displayName": "Sonos group",
"interfaces": ["gateway", "connectable"],
"createMethods": ["discovery"],
"paramTypes": [
"name": "sonosConnection",
"displayName": "Sonos connection",
"interfaces": ["gateway"],
"createMethods": ["user"],
"setupMethod": "userandpassword",
"stateTypes": [
{
"id": "5aa4360c-61de-47d0-a72e-a19d57712e1c",
"name": "connected",
"displayName": "connected",
"displayNameEvent": "connected changed",
"defaultValue": false,
"type": "bool"
}
]
},
@ -29,8 +36,8 @@
"paramTypes": [
{
"id": "defc44cd-2ffb-4af1-b348-d6a3474c7515",
"name": "zoneName",
"displayName": "Zone name",
"name": "groupId",
"displayName": "Group id",
"type" : "QString"
}
],

221
sonos/oauth.cpp Normal file
View File

@ -0,0 +1,221 @@
#include "oauth.h"
#include "extern-plugininfo.h"
#include <QNetworkRequest>
#include <QJsonDocument>
#include <QUuid>
OAuth::OAuth(QString clientId, QObject *parent) :
QObject(parent),
m_clientId(clientId),
m_authenticated(false)
{
m_networkManager = new QNetworkAccessManager(this);
connect(m_networkManager, &QNetworkAccessManager::finished, this, &OAuth::replyFinished);
m_timer = new QTimer(this);
m_timer->setSingleShot(false);
connect(m_timer, &QTimer::timeout, this, &OAuth::refreshTimeout);
}
QUrl OAuth::url() const
{
return m_url;
}
void OAuth::setUrl(const QUrl &url)
{
m_url = url;
}
QUrlQuery OAuth::query() const
{
return m_query;
}
void OAuth::setQuery(const QUrlQuery &query)
{
m_query = query;
}
QString OAuth::clientId() const
{
return m_clientId;
}
void OAuth::setClientId(const QString &clientId)
{
m_clientId = clientId;
}
QString OAuth::scope() const
{
return m_scope;
}
void OAuth::setScope(const QString &scope)
{
m_scope = scope;
}
QString OAuth::authorizationCode() const
{
return m_token;
}
QString OAuth::bearerToken() const
{
return m_token;
}
bool OAuth::authenticated() const
{
return m_authenticated;
}
void OAuth::startAuthentication()
{
qCDebug(dcSonos) << "Start authentication";
QUrlQuery query;
query.addQueryItem("client_id", m_clientId);
query.addQueryItem("redirect_uri", m_redirectUri);
query.addQueryItem("response_type", "code");
query.addQueryItem("scope", m_scope);
m_state = QUuid().toByteArray();
query.addQueryItem("state", m_state);
setQuery(query);
QNetworkRequest request(m_url);
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded; charset=UTF-8");
m_tokenRequests.append(m_networkManager->post(request, m_query.toString().toUtf8()));
}
void OAuth::setAuthenticated(const bool &authenticated)
{
if (authenticated) {
qCDebug(dcSonos()) << "Authenticated successfully";
} else {
m_timer->stop();
qCWarning(dcSonos) << "Authentication failed";
}
m_authenticated = authenticated;
emit authenticationChanged();
}
void OAuth::setToken(const QString &token)
{
m_token = token;
emit tokenChanged();
}
void OAuth::replyFinished(QNetworkReply *reply)
{
int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
reply->deleteLater();
// token request
if (m_tokenRequests.contains(reply)) {
QByteArray data = reply->readAll();
m_tokenRequests.removeAll(reply);
// check HTTP status code
if (status != 200) {
qCWarning(dcSonos) << "Request token reply HTTP error:" << status << reply->errorString();
qCWarning(dcSonos) << data;
setAuthenticated(false);
return;
}
// check JSON
QJsonParseError error;
QJsonDocument jsonDoc = QJsonDocument::fromJson(data, &error);
if (error.error != QJsonParseError::NoError) {
qCWarning(dcSonos) << "Request token reply JSON error:" << error.errorString();
setAuthenticated(false);
return;
}
if (!jsonDoc.toVariant().toMap().contains("code")) {
qCWarning(dcSonos) << "Could not get code" << jsonDoc.toJson();
setAuthenticated(false);
return;
}
if (!jsonDoc.toVariant().toMap().contains("state")) {
qCWarning(dcSonos) << "Could not get state" << jsonDoc.toJson();
return;
}
if (jsonDoc.toVariant().toMap().value("state").toString() != m_state) {
qCWarning(dcSonos) << "State doesn't match. Expected:" << m_state << "Received:" << jsonDoc.toVariant().toMap().value("state").toString();
}
setToken(jsonDoc.toVariant().toMap().value("code").toString());
setAuthenticated(true);
if (jsonDoc.toVariant().toMap().contains("expires_in") && jsonDoc.toVariant().toMap().contains("refresh_token")) {
int expireTime = jsonDoc.toVariant().toMap().value("expires_in").toInt();
m_refreshToken = jsonDoc.toVariant().toMap().value("refresh_token").toString();
qCDebug(dcSonos) << "Token will be refreshed in" << expireTime << "[s]";
m_timer->start((expireTime - 20) * 1000);
}
} else if (m_refreshTokenRequests.contains(reply)) {
QByteArray data = reply->readAll();
m_refreshTokenRequests.removeAll(reply);
// check HTTP status code
if (status != 200) {
qCWarning(dcSonos) << "Refresh token reply HTTP error:" << status << reply->errorString();
qCWarning(dcSonos) << data;
setAuthenticated(false);
return;
}
// check JSON
QJsonParseError error;
QJsonDocument jsonDoc = QJsonDocument::fromJson(data, &error);
if (error.error != QJsonParseError::NoError) {
qCWarning(dcSonos) << "Refresh token reply JSON error:" << error.errorString();
setAuthenticated(false);
return;
}
if (!jsonDoc.toVariant().toMap().contains("access_token")) {
qCWarning(dcSonos) << "Could not get access token after refresh" << jsonDoc.toJson();
setAuthenticated(false);
return;
}
setToken(jsonDoc.toVariant().toMap().value("access_token").toString());
qCDebug(dcSonos) << "Token refreshed successfully";
if (jsonDoc.toVariant().toMap().contains("expires_in") && jsonDoc.toVariant().toMap().contains("refresh_token")) {
int expireTime = jsonDoc.toVariant().toMap().value("expires_in").toInt();
m_refreshToken = jsonDoc.toVariant().toMap().value("refresh_token").toString();
qCDebug(dcSonos) << "Token will be refreshed in" << expireTime << "[s]";
m_timer->start((expireTime - 20) * 1000);
}
if (!authenticated())
setAuthenticated(true);
}
}
void OAuth::refreshTimeout()
{
qCDebug(dcSonos) << "Refresh authentication token for";
QUrlQuery query;
query.addQueryItem("grant_type", "refresh_token");
query.addQueryItem("refresh_token", m_refreshToken);
query.addQueryItem("client_id", m_clientId);
QNetworkRequest request(m_url);
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded; charset=UTF-8");
m_refreshTokenRequests.append(m_networkManager->post(request, query.toString().toUtf8()));
}

71
sonos/oauth.h Normal file
View File

@ -0,0 +1,71 @@
#ifndef OAUTH_H
#define OAUTH_H
#include <QObject>
#include <QString>
#include <QTimer>
#include <QUrl>
#include <QUrlQuery>
#include <QNetworkAccessManager>
#include <QNetworkReply>
class OAuth : public QObject
{
Q_OBJECT
public:
explicit OAuth(QString clientId, QObject *parent = nullptr);
QUrl url() const;
void setUrl(const QUrl &url);
QUrlQuery query() const;
void setQuery(const QUrlQuery &query);
QString clientId() const;
void setClientId(const QString &clientId);
QString scope() const;
void setScope(const QString &scope);
QByteArray authorizationCode() const;
QByteArray bearerToken() const;
bool authenticated() const;
void startAuthentication();
private:
QNetworkAccessManager *m_networkManager;
QTimer *m_timer;
QList<QNetworkReply *> m_tokenRequests;
QList<QNetworkReply *> m_refreshTokenRequests;
QUrl m_url;
QUrlQuery m_query;
QString m_clientId;
QString m_scope;
QString m_state;
QString m_redirectUri;
QString m_responseType;
QString m_token;
QString m_refreshToken;
bool m_authenticated;
void setAuthenticated(const bool &authenticated);
void setToken(const QString &token);
private slots:
void replyFinished(QNetworkReply *reply);
void refreshTimeout();
signals:
void authenticationChanged();
void tokenChanged();
};
#endif // OAUTH_H

View File

@ -22,26 +22,33 @@
#include "sonos.h"
#include "extern-plugininfo.h"
#include "network/networkaccessmanager.h"
#include <QJsonDocument>
Sonos::Sonos(QObject *parent)
Sonos::Sonos(QByteArray apiKey, QObject *parent) :
QObject(parent),
m_apiKey(apiKey)
{
}
void Sonos::authenticate(const QString &username, const QString &password)
{
//get oauth autherisation
Q_UNUSED(username)
Q_UNUSED(password)
//get accesst token
m_OAuth = new OAuth(m_apiKey, this);
m_OAuth->setUrl(QUrl(m_baseAuthorizationUrl));
m_OAuth->setScope("playback-control-all");
m_OAuth->startAuthentication();
}
void Sonos::getHouseholds()
{
QNetworkRequest request;
request.setHeader(QNetworkRequest::KnownHeaders::ContentTypeHeader, "application/json");
request.setRawHeader("Authorization", "Bearer" + m_bearerToken);
request.setUrl(m_baseControlUrl + "/households");
request.setRawHeader("Authorization", "Bearer" + m_OAuth->bearerToken());
request.setUrl(QUrl(m_baseControlUrl + "/households"));
QNetworkReply *reply = QNetworkAccessManager.get(request);
connect(reply, &QNetworkReply::finished, this [this] {
int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
@ -61,12 +68,3 @@ void Sonos::getHouseholds()
});
}
void Sonos::getPlayerVolume(int playerId)
{
QNetworkRequest request;
QJsonObject object;
object.
}

View File

@ -27,6 +27,7 @@
#include <QNetworkAccessManager>
#include "devices/device.h"
#include "oauth.h"
class Sonos : public QObject
{
@ -135,8 +136,7 @@ public:
{
QString name;
ArtistObject artist;
QString
//TODO
};
struct ContainerObject
@ -150,6 +150,8 @@ public:
};
explicit Sonos(QByteArray apiKey, QObject *parent = nullptr);
void setApiKey(QByteArray apiKey);
void authenticate(const QString &username, const QString &password);
void getHouseholds();
@ -201,14 +203,17 @@ public:
void setPlayerSettings();
private:
QUrl m_baseAuthorizationUrl = "api.sonos.com/login/v3/oauth";
QUrl m_baseControlUrl = "api.ws.sonos.com/control/api/v1";
QByteArray m_baseAuthorizationUrl = "api.sonos.com/login/v3/oauth";
QByteArray m_baseControlUrl = "api.ws.sonos.com/control/api/v1";
QByteArray m_apiKey;
OAuth *m_OAuth = nullptr;
private slots:
signals:
void authenticationSuccessfull();
void authenticationFinished();
void authenticationFailed(const QString &reason);

View File

@ -7,9 +7,11 @@ TARGET = $$qtLibraryTarget(nymea_devicepluginsonos)
SOURCES += \
devicepluginsonos.cpp \
sonos.cpp \
oauth.cpp
HEADERS += \
devicepluginsonos.h \
sonos.h \
oauth.h