diff --git a/sonos/devicepluginsonos.cpp b/sonos/devicepluginsonos.cpp index c9562d33..24f61306 100644 --- a/sonos/devicepluginsonos.cpp +++ b/sonos/devicepluginsonos.cpp @@ -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 ¶ms) -{ - Q_UNUSED(params) - Q_UNUSED(deviceClassId) - - - if (!m_sonosSystem->Discover()) { - return Device::DeviceErrorDeviceNotFound; - } - - QList 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; } diff --git a/sonos/devicepluginsonos.h b/sonos/devicepluginsonos.h index 2b4e9377..3fb8f1da 100644 --- a/sonos/devicepluginsonos.h +++ b/sonos/devicepluginsonos.h @@ -48,7 +48,9 @@ public: Device::DeviceError executeAction(Device *device, const Action &action) override; private: - Sonos *sonos = nullptr; + PluginTimer *m_pluginTimer = nullptr; + QHash m_sonosConnections; + private slots: void onPluginTimer(); diff --git a/sonos/devicepluginsonos.json b/sonos/devicepluginsonos.json index 7992e42a..68daca04 100644 --- a/sonos/devicepluginsonos.json +++ b/sonos/devicepluginsonos.json @@ -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" } ], diff --git a/sonos/oauth.cpp b/sonos/oauth.cpp new file mode 100644 index 00000000..2c34c2b9 --- /dev/null +++ b/sonos/oauth.cpp @@ -0,0 +1,221 @@ +#include "oauth.h" +#include "extern-plugininfo.h" + +#include +#include +#include + +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())); +} diff --git a/sonos/oauth.h b/sonos/oauth.h new file mode 100644 index 00000000..205eda51 --- /dev/null +++ b/sonos/oauth.h @@ -0,0 +1,71 @@ +#ifndef OAUTH_H +#define OAUTH_H + + +#include +#include +#include +#include +#include +#include +#include + +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 m_tokenRequests; + QList 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 diff --git a/sonos/sonos.cpp b/sonos/sonos.cpp index 919c9c47..3263085b 100644 --- a/sonos/sonos.cpp +++ b/sonos/sonos.cpp @@ -22,26 +22,33 @@ #include "sonos.h" #include "extern-plugininfo.h" +#include "network/networkaccessmanager.h" #include -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. -} - - diff --git a/sonos/sonos.h b/sonos/sonos.h index 832ab065..8ff3e0c4 100644 --- a/sonos/sonos.h +++ b/sonos/sonos.h @@ -27,6 +27,7 @@ #include #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); diff --git a/sonos/sonos.pro b/sonos/sonos.pro index a107f02e..7043560f 100644 --- a/sonos/sonos.pro +++ b/sonos/sonos.pro @@ -7,9 +7,11 @@ TARGET = $$qtLibraryTarget(nymea_devicepluginsonos) SOURCES += \ devicepluginsonos.cpp \ sonos.cpp \ + oauth.cpp HEADERS += \ devicepluginsonos.h \ sonos.h \ + oauth.h