diff --git a/debian/control b/debian/control
index 8f0b01db..9058a0ba 100644
--- a/debian/control
+++ b/debian/control
@@ -705,6 +705,22 @@ Description: nymea.io plugin for senic
This package will install the nymea.io plugin for senic
+Package: nymea-plugin-sonos
+Architecture: any
+Depends: ${shlibs:Depends},
+ ${misc:Depends},
+ nymea-plugins-translations,
+Replaces: guh-plugin-sonos
+Description: nymea.io plugin for Sonos smart speakers
+ The nymea daemon is a plugin based IoT (Internet of Things) server. The
+ server works like a translator for devices, things and services and
+ allows them to interact.
+ With the powerful rule engine you are able to connect any device available
+ in the system and create individual scenes and behaviors for your environment.
+ .
+ This package will install the nymea.io plugin for Sonos smart speakers
+
+
Package: nymea-plugin-snapd
Architecture: any
Depends: ${shlibs:Depends},
@@ -821,6 +837,7 @@ Depends: nymea-plugin-awattar,
nymea-plugin-wemo,
nymea-plugin-elgato,
nymea-plugin-senic,
+ nymea-plugin-sonos,
nymea-plugin-keba,
Replaces: guh-plugins
Description: Plugins for nymea IoT server - the default plugin collection
diff --git a/debian/nymea-plugin-sonos.install.in b/debian/nymea-plugin-sonos.install.in
new file mode 100644
index 00000000..b5a16a91
--- /dev/null
+++ b/debian/nymea-plugin-sonos.install.in
@@ -0,0 +1 @@
+usr/lib/@DEB_HOST_MULTIARCH@/nymea/plugins/libnymea_devicepluginsonos.so
diff --git a/nymea-plugins.pro b/nymea-plugins.pro
index c5d3f1ae..4954e159 100644
--- a/nymea-plugins.pro
+++ b/nymea-plugins.pro
@@ -41,6 +41,7 @@ PLUGIN_DIRS = \
serialportcommander \
simulation \
snapd \
+ sonos \
tasmota \
tcpcommander \
texasinstruments \
diff --git a/sonos/devicepluginsonos.cpp b/sonos/devicepluginsonos.cpp
new file mode 100644
index 00000000..04b08355
--- /dev/null
+++ b/sonos/devicepluginsonos.cpp
@@ -0,0 +1,653 @@
+/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+ * *
+ * Copyright (C) 2019 Bernhard Trinnes . *
+ * *
+ * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
+
+#include "devicepluginsonos.h"
+#include "devices/device.h"
+#include "network/networkaccessmanager.h"
+#include "plugininfo.h"
+#include "types/mediabrowseritem.h"
+
+
+#include
+#include
+#include
+#include
+
+DevicePluginSonos::DevicePluginSonos()
+{
+}
+
+
+DevicePluginSonos::~DevicePluginSonos()
+{
+ if (m_pluginTimer5sec)
+ hardwareManager()->pluginTimerManager()->unregisterTimer(m_pluginTimer5sec);
+ if (m_pluginTimer60sec)
+ hardwareManager()->pluginTimerManager()->unregisterTimer(m_pluginTimer60sec);
+}
+
+
+void DevicePluginSonos::setupDevice(DeviceSetupInfo *info)
+{
+ Device *device = info->device();
+
+ if (device->deviceClassId() == sonosConnectionDeviceClassId) {
+ Sonos *sonos;
+ if (m_setupSonosConnections.keys().contains(device->id())) {
+ //Fresh device setup, has already a fresh access token
+ qCDebug(dcSonos()) << "Sonos OAuth setup complete";
+ sonos = m_setupSonosConnections.take(device->id());
+ connect(sonos, &Sonos::connectionChanged, this, &DevicePluginSonos::onConnectionChanged);
+ connect(sonos, &Sonos::householdIdsReceived, this, &DevicePluginSonos::onHouseholdIdsReceived);
+ connect(sonos, &Sonos::groupsReceived, this, &DevicePluginSonos::onGroupsReceived);
+ connect(sonos, &Sonos::playBackStatusReceived, this, &DevicePluginSonos::onPlayBackStatusReceived);
+ connect(sonos, &Sonos::metadataStatusReceived, this, &DevicePluginSonos::onMetadataStatusReceived);
+ connect(sonos, &Sonos::volumeReceived, this, &DevicePluginSonos::onVolumeReceived);
+ connect(sonos, &Sonos::actionExecuted, this, &DevicePluginSonos::onActionExecuted);
+ connect(sonos, &Sonos::authenticationStatusChanged, this, &DevicePluginSonos::onAuthenticationStatusChanged);
+
+ connect(sonos, &Sonos::authenticationStatusChanged, info, [info](bool authenticated){
+ if (authenticated) {
+ info->finish(Device::DeviceErrorNoError);
+ } else {
+ info->finish(Device::DeviceErrorAuthenticationFailure);
+ }
+ });
+
+ m_sonosConnections.insert(device, sonos);
+ return info->finish(Device::DeviceErrorNoError);
+ } else {
+ //device loaded from the device database, needs a new access token;
+ pluginStorage()->beginGroup(device->id().toString());
+ QByteArray refreshToken = pluginStorage()->value("refresh_token").toByteArray();
+ pluginStorage()->endGroup();
+
+ if (refreshToken.isEmpty()) {
+ info->finish(Device::DeviceErrorAuthenticationFailure);
+ return;
+ }
+
+ sonos = new Sonos(hardwareManager()->networkManager(), "0a8f6d44-d9d1-4474-bcfa-cfb41f8b66e8", "3095ce48-0c5d-47ce-a1f4-6005c7b8fdb5", this);
+ connect(sonos, &Sonos::connectionChanged, this, &DevicePluginSonos::onConnectionChanged);
+ connect(sonos, &Sonos::householdIdsReceived, this, &DevicePluginSonos::onHouseholdIdsReceived);
+ connect(sonos, &Sonos::groupsReceived, this, &DevicePluginSonos::onGroupsReceived);
+ connect(sonos, &Sonos::playBackStatusReceived, this, &DevicePluginSonos::onPlayBackStatusReceived);
+ connect(sonos, &Sonos::metadataStatusReceived, this, &DevicePluginSonos::onMetadataStatusReceived);
+ connect(sonos, &Sonos::volumeReceived, this, &DevicePluginSonos::onVolumeReceived);
+ connect(sonos, &Sonos::actionExecuted, this, &DevicePluginSonos::onActionExecuted);
+ connect(sonos, &Sonos::authenticationStatusChanged, this, &DevicePluginSonos::onAuthenticationStatusChanged);
+ connect(sonos, &Sonos::favoritesReceived, this, &DevicePluginSonos::onFavoritesReceived);
+ sonos->getAccessTokenFromRefreshToken(refreshToken);
+ m_sonosConnections.insert(device, sonos);
+ return info->finish(Device::DeviceErrorNoError);
+ }
+ }
+
+ if (device->deviceClassId() == sonosGroupDeviceClassId) {
+ return info->finish(Device::DeviceErrorNoError);
+ }
+
+ qCWarning(dcSonos()) << "Unhandled device class id in setupDevice" << device->deviceClassId();
+}
+
+void DevicePluginSonos::startPairing(DevicePairingInfo *info)
+{
+ if (info->deviceClassId() == sonosConnectionDeviceClassId) {
+
+ Sonos *sonos = new Sonos(hardwareManager()->networkManager(), "0a8f6d44-d9d1-4474-bcfa-cfb41f8b66e8", "3095ce48-0c5d-47ce-a1f4-6005c7b8fdb5", this);
+ QUrl url = sonos->getLoginUrl(QUrl("https://127.0.0.1:8888"));
+ qCDebug(dcSonos()) << "Sonos url:" << url;
+ info->setOAuthUrl(url);
+ info->finish(Device::DeviceErrorNoError);
+ m_setupSonosConnections.insert(info->deviceId(), sonos);
+ return;
+ }
+
+ qCWarning(dcSonos()) << "Unhandled pairing metod!";
+ info->finish(Device::DeviceErrorCreationMethodNotSupported);
+}
+
+void DevicePluginSonos::confirmPairing(DevicePairingInfo *info, const QString &username, const QString &secret)
+{
+ Q_UNUSED(username)
+
+ if (info->deviceClassId() == sonosConnectionDeviceClassId) {
+ qCDebug(dcSonos()) << "Redirect url is" << secret;
+ QUrl url(secret);
+ QUrlQuery query(url);
+ QByteArray authorizationCode = query.queryItemValue("code").toLocal8Bit();
+ QByteArray state = query.queryItemValue("state").toLocal8Bit();
+ //TODO evaluate state if it equals the given state
+
+ Sonos *sonos = m_setupSonosConnections.value(info->deviceId());
+
+ if (!sonos) {
+ qWarning(dcSonos()) << "No sonos connection found for device:" << info->deviceName();
+ m_setupSonosConnections.remove(info->deviceId());
+ sonos->deleteLater();
+ info->finish(Device::DeviceErrorHardwareFailure);
+ return;
+ }
+ sonos->getAccessTokenFromAuthorizationCode(authorizationCode);
+ connect(sonos, &Sonos::authenticationStatusChanged, info, [this, info, sonos](bool authenticated){
+ if(!authenticated) {
+ qWarning(dcSonos()) << "Authentication process failed" << info->deviceName();
+ m_setupSonosConnections.remove(info->deviceId());
+ sonos->deleteLater();
+ info->finish(Device::DeviceErrorSetupFailed, QT_TR_NOOP("Authentication failed. Please try again."));
+ return;
+ }
+ QByteArray accessToken = sonos->accessToken();
+ QByteArray refreshToken = sonos->refreshToken();
+ qCDebug(dcSonos()) << "Token:" << accessToken << refreshToken;
+
+ pluginStorage()->beginGroup(info->deviceId().toString());
+ pluginStorage()->setValue("refresh_token", refreshToken);
+ pluginStorage()->endGroup();
+
+ info->finish(Device::DeviceErrorNoError);
+ });
+ return;
+ }
+ qCWarning(dcSonos()) << "Invalid deviceclassId -> no pairing possible with this device";
+ info->finish(Device::DeviceErrorDeviceClassNotFound);
+}
+
+void DevicePluginSonos::postSetupDevice(Device *device)
+{
+ if (!m_pluginTimer5sec) {
+ m_pluginTimer5sec = hardwareManager()->pluginTimerManager()->registerTimer(5);
+ connect(m_pluginTimer5sec, &PluginTimer::timeout, this, [this]() {
+
+ foreach (Device *connectionDevice, myDevices().filterByDeviceClassId(sonosConnectionDeviceClassId)) {
+ Sonos *sonos = m_sonosConnections.value(connectionDevice);
+ if (!sonos) {
+ qWarning(dcSonos()) << "No sonos connection found to device" << connectionDevice->name();
+ continue;
+ }
+ foreach (Device *groupDevice, myDevices().filterByParentDeviceId(connectionDevice->id())) {
+ if (groupDevice->deviceClassId() == sonosGroupDeviceClassId) {
+ //get playback status of each group
+ QString groupId = groupDevice->paramValue(sonosGroupDeviceGroupIdParamTypeId).toString();
+ sonos->getGroupPlaybackStatus(groupId);
+ sonos->getGroupMetadataStatus(groupId);
+ sonos->getGroupVolume(groupId);
+ }
+ }
+ }
+ });
+ }
+
+ if (!m_pluginTimer60sec) {
+ m_pluginTimer60sec = hardwareManager()->pluginTimerManager()->registerTimer(60);
+ connect(m_pluginTimer60sec, &PluginTimer::timeout, this, [this]() {
+ foreach (Device *device, myDevices().filterByDeviceClassId(sonosConnectionDeviceClassId)) {
+ Sonos *sonos = m_sonosConnections.value(device);
+ if (!sonos) {
+ qWarning(dcSonos()) << "No sonos connection found to device" << device->name();
+ continue;
+ }
+ //get groups for each household in order to add or remove groups
+ sonos->getHouseholds();
+ }
+ });
+ }
+
+ if (device->deviceClassId() == sonosConnectionDeviceClassId) {
+ Sonos *sonos = m_sonosConnections.value(device);
+ sonos->getHouseholds();
+ }
+
+ if (device->deviceClassId() == sonosGroupDeviceClassId) {
+ Device *parentDevice = myDevices().findById(device->parentId());
+ Sonos *sonos = m_sonosConnections.value(parentDevice);
+ if (!sonos) {
+ return;
+ }
+ QString groupId = device->paramValue(sonosGroupDeviceGroupIdParamTypeId).toString();
+ sonos->getGroupPlaybackStatus(groupId);
+ sonos->getGroupMetadataStatus(groupId);
+ sonos->getGroupVolume(groupId);
+ }
+}
+
+
+void DevicePluginSonos::startMonitoringAutoDevices()
+{
+ foreach (Device *device, myDevices()) {
+ if (device->deviceClassId() == sonosGroupDeviceClassId) {
+ return;
+ }
+ }
+}
+
+void DevicePluginSonos::deviceRemoved(Device *device)
+{
+ qCDebug(dcSonos) << "Delete " << device->name();
+ if (myDevices().empty()) {
+ hardwareManager()->pluginTimerManager()->unregisterTimer(m_pluginTimer5sec);
+ hardwareManager()->pluginTimerManager()->unregisterTimer(m_pluginTimer60sec);
+ m_pluginTimer5sec = nullptr;
+ m_pluginTimer60sec = nullptr;
+ }
+}
+
+
+void DevicePluginSonos::executeAction(DeviceActionInfo *info)
+{
+ Device *device = info->device();
+ Action action = info->action();
+
+ if (device->deviceClassId() == sonosGroupDeviceClassId) {
+ Sonos *sonos = m_sonosConnections.value(myDevices().findById(device->parentId()));
+ QString groupId = device->paramValue(sonosGroupDeviceGroupIdParamTypeId).toString();
+
+ if (!sonos) {
+ qWarning(dcSonos()) << "Action cannot be executed: Sonos connection not available";
+ return info->finish(Device::DeviceErrorHardwareNotAvailable, QT_TR_NOOP("Sonos device is not available."));
+ }
+
+ if (action.actionTypeId() == sonosGroupPlayActionTypeId) {
+ m_pendingActions.insert(sonos->groupPlay(groupId), QPointer(info));
+ return;
+ }
+
+ if (action.actionTypeId() == sonosGroupShuffleActionTypeId) {
+ bool shuffle = action.param(sonosGroupShuffleActionShuffleParamTypeId).value().toBool();
+ m_pendingActions.insert(sonos->groupSetShuffle(groupId, shuffle), QPointer(info));
+ return;
+ }
+
+ if (action.actionTypeId() == sonosGroupRepeatActionTypeId) {
+ if (action.param(sonosGroupRepeatActionRepeatParamTypeId).value().toString() == "None") {
+ m_pendingActions.insert(sonos->groupSetRepeat(groupId, Sonos::RepeatModeNone), QPointer(info));
+ } else if (action.param(sonosGroupRepeatActionRepeatParamTypeId).value().toString() == "One") {
+ m_pendingActions.insert(sonos->groupSetRepeat(groupId, Sonos::RepeatModeOne), QPointer(info));
+ } else if (action.param(sonosGroupRepeatActionRepeatParamTypeId).value().toString() == "All") {
+ m_pendingActions.insert(sonos->groupSetRepeat(groupId, Sonos::RepeatModeAll), QPointer(info));
+ } else {
+ return info->finish(Device::DeviceErrorHardwareFailure);
+ }
+ return;
+ }
+
+ if (action.actionTypeId() == sonosGroupPauseActionTypeId) {
+ m_pendingActions.insert(sonos->groupPause(groupId), QPointer(info));
+ return;
+ }
+
+ if (action.actionTypeId() == sonosGroupStopActionTypeId) {
+ m_pendingActions.insert(sonos->groupPause(groupId), QPointer(info));
+ return;
+ }
+
+ if (action.actionTypeId() == sonosGroupMuteActionTypeId) {
+ bool mute = action.param(sonosGroupMuteActionMuteParamTypeId).value().toBool();
+ m_pendingActions.insert(sonos->setGroupMute(groupId, mute), QPointer(info));
+ return;
+ }
+
+
+ if (action.actionTypeId() == sonosGroupVolumeActionTypeId) {
+ int volume = action.param(sonosGroupVolumeActionVolumeParamTypeId).value().toInt();
+ m_pendingActions.insert(sonos->setGroupVolume(groupId, volume), QPointer(info));
+ return;
+ }
+
+ if (action.actionTypeId() == sonosGroupSkipNextActionTypeId) {
+ m_pendingActions.insert(sonos->groupSkipToNextTrack(groupId), QPointer(info));
+ return;
+ }
+
+ if (action.actionTypeId() == sonosGroupSkipBackActionTypeId) {
+ m_pendingActions.insert(sonos->groupSkipToPreviousTrack(groupId), QPointer(info));
+ return;
+ }
+
+ if (action.actionTypeId() == sonosGroupPlaybackStatusActionTypeId) {
+ QString playbackStatus = action.param(sonosGroupPlaybackStatusActionPlaybackStatusParamTypeId).value().toString();
+ if (playbackStatus == "Playing") {
+ m_pendingActions.insert(sonos->groupPlay(groupId), QPointer(info));
+ } else if(playbackStatus == "Stopped") {
+ m_pendingActions.insert(sonos->groupPause(groupId), QPointer(info));
+ } else if(playbackStatus == "Paused") {
+ m_pendingActions.insert(sonos->groupPause(groupId), QPointer(info));
+ }
+ return;
+ }
+ return info->finish(Device::DeviceErrorActionTypeNotFound);
+ }
+ info->finish(Device::DeviceErrorDeviceClassNotFound);
+}
+
+void DevicePluginSonos::browseDevice(BrowseResult *result)
+{
+ Device *parentDevice = myDevices().findById(result->device()->parentId());
+ Sonos *sonosConnection = m_sonosConnections.value(parentDevice);
+ if (!sonosConnection) {
+ result->finish(Device::DeviceErrorHardwareNotAvailable);
+ return;
+ }
+
+ qDebug(dcSonos()) << "Browse Device" << result->itemId();
+ QString householdId = result->device()->paramValue(sonosGroupDeviceHouseholdIdParamTypeId).toString();
+ if (result->itemId().isEmpty()){
+ BrowserItem item;
+ item.setId(m_browseFavoritesPrefix);
+ item.setIcon(BrowserItem::BrowserIconFavorites);
+ item.setExecutable(false);
+ item.setBrowsable(true);
+ item.setDisplayName("Favorites");
+ result->addItem(item);
+ result->finish(Device::DeviceErrorNoError);
+ } else if (result->itemId() == m_browseFavoritesPrefix) {
+ QUuid requestId = sonosConnection->getFavorites(householdId);
+ m_pendingBrowseResult.insert(requestId, result);
+ connect(result, &BrowseResult::aborted,[requestId, this](){m_pendingBrowseResult.remove(requestId);});
+ } else {
+ //TODO add media browsing
+ result->finish(Device::DeviceErrorItemNotFound);
+ }
+}
+
+void DevicePluginSonos::browserItem(BrowserItemResult *result)
+{
+ Device *parentDevice = myDevices().findById(result->device()->parentId());
+ Sonos *sonosConnection = m_sonosConnections.value(parentDevice);
+ if (!sonosConnection) {
+ result->finish(Device::DeviceErrorHardwareNotAvailable);
+ return;
+ }
+
+ qCDebug(dcSonos()) << "Browser Item" << result->itemId();
+ QString householdId = result->device()->paramValue(sonosGroupDeviceHouseholdIdParamTypeId).toString();
+ if (result->itemId().startsWith(m_browseFavoritesPrefix)) {
+ QUuid requestId = sonosConnection->getFavorites(householdId);
+ m_pendingBrowserItemResult.insert(requestId, result);
+ connect(result, &BrowserItemResult::aborted, [requestId, this](){m_pendingBrowserItemResult.remove(requestId);});
+ } else {
+ //TODO add media browsing
+ result->finish(Device::DeviceErrorItemNotFound);
+ }
+}
+
+void DevicePluginSonos::executeBrowserItem(BrowserActionInfo *info)
+{
+ Device *parentDevice = myDevices().findById(info->device()->parentId());
+ Sonos *sonosConnection = m_sonosConnections.value(parentDevice);
+ if (!sonosConnection)
+ return;
+
+ QString groupId = info->device()->paramValue(sonosGroupDeviceGroupIdParamTypeId).toString();
+ if (info->browserAction().itemId().startsWith(m_browseFavoritesPrefix)) {
+ QString favoriteId = info->browserAction().itemId().remove(m_browseFavoritesPrefix);
+ favoriteId.remove('/');
+ QUuid requestId = sonosConnection->loadFavorite(groupId, favoriteId);
+ m_pendingBrowserExecution.insert(requestId, info);
+ connect(info, &BrowserActionInfo::aborted,[requestId, this](){m_pendingBrowserExecution.remove(requestId);});
+ } else {
+ //TODO add media browsing
+ info->finish(Device::DeviceErrorItemNotFound);
+ }
+}
+
+void DevicePluginSonos::onConnectionChanged(bool connected)
+{
+ Sonos *sonos = static_cast(sender());
+ Device *device = m_sonosConnections.key(sonos);
+ if (!device)
+ return;
+ device->setStateValue(sonosConnectionConnectedStateTypeId, connected);
+
+ foreach (Device *groupDevice, myDevices().filterByParentDeviceId(device->id())) {
+ groupDevice->setStateValue(sonosGroupConnectedStateTypeId, connected);
+ }
+}
+
+void DevicePluginSonos::onAuthenticationStatusChanged(bool authenticated)
+{
+ Sonos *sonosConnection = static_cast(sender());
+ Device *device = m_sonosConnections.key(sonosConnection);
+ if (!device)
+ return;
+
+ device->setStateValue(sonosConnectionLoggedInStateTypeId, authenticated);
+ if (!authenticated) {
+ //refresh access token needs to be refreshed
+ pluginStorage()->beginGroup(device->id().toString());
+ QByteArray refreshToken = pluginStorage()->value("refresh_token").toByteArray();
+ pluginStorage()->endGroup();
+ sonosConnection->getAccessTokenFromRefreshToken(refreshToken);
+ }
+}
+
+void DevicePluginSonos::onHouseholdIdsReceived(QList householdIds)
+{
+ Sonos *sonos = static_cast(sender());
+ foreach(QString householdId, householdIds) {
+ sonos->getGroups(householdId);
+ sonos->getPlaylists(householdId);
+ }
+}
+
+void DevicePluginSonos::onFavoritesReceived(QUuid requestId, const QString &householdId, QList favorites)
+{
+ Q_UNUSED(householdId)
+
+ if (m_pendingBrowseResult.contains(requestId)) {
+ BrowseResult *result = m_pendingBrowseResult.take(requestId);
+ if (!result)
+ return;
+
+ foreach(Sonos::FavoriteObject favorite, favorites) {
+ MediaBrowserItem item;
+ item.setId(result->itemId() + "/" + favorite.id);
+ item.setExecutable(true);
+ item.setBrowsable(false);
+ if (!favorite.imageUrl.isEmpty()) {
+ item.setThumbnail(favorite.imageUrl);
+ } else {
+ item.setIcon(BrowserItem::BrowserIconFavorites);
+ }
+ item.setDisplayName(favorite.name);
+ item.setDescription(favorite.description);
+ result->addItem(item);
+ qDebug(dcSonos()) << "Favorite: " << favorite.name << favorite.description;
+ }
+ result->finish(Device::DeviceErrorNoError);
+
+ } else if (m_pendingBrowserItemResult.contains(requestId)) {
+ BrowserItemResult *result = m_pendingBrowserItemResult.take(requestId);
+ if (!result)
+ return;
+ QString favoriteId = result->itemId().remove(m_browseFavoritesPrefix);
+ favoriteId.remove('/');
+
+ foreach(Sonos::FavoriteObject favorite, favorites) {
+ if (favorite.id == favoriteId) {
+ MediaBrowserItem item;
+ item.setId(result->itemId());
+ item.setExecutable(true);
+ item.setBrowsable(false);
+ if (!favorite.imageUrl.isEmpty()) {
+ item.setThumbnail(favorite.imageUrl);
+ } else {
+ item.setIcon(BrowserItem::BrowserIconFavorites);
+ }
+ item.setDisplayName(favorite.name);
+ item.setDescription(favorite.description);
+ result->finish(item);
+ return;
+ }
+ }
+ }
+}
+
+void DevicePluginSonos::onPlaylistsReceived(const QString &householdId, QList playlists)
+{
+ Sonos *sonos = static_cast(sender());
+
+ foreach(Sonos::PlaylistObject playlist, playlists) {
+ qDebug(dcSonos()) << "Playlist: " << playlist.name << playlist.type << playlist.trackCount;
+ sonos->getPlaylist(householdId, playlist.id); //Get the playlist details
+ }
+}
+
+void DevicePluginSonos::onPlaylistSummaryReceived(const QString &householdId, Sonos::PlaylistSummaryObject playlistSummary)
+{
+ Q_UNUSED(householdId);
+ qDebug(dcSonos()) << "Playlist summary received: " << playlistSummary.name;
+ foreach(Sonos::PlaylistTrackObject track, playlistSummary.tracks) {
+ qDebug(dcSonos()) << "---- Track: " << track.name << track.album << track.artist;
+ }
+}
+
+void DevicePluginSonos::onGroupsReceived(const QString &householdId, QList groupObjects)
+{
+ Sonos *sonos = static_cast(sender());
+ Device *parentDevice = m_sonosConnections.key(sonos);
+ if (!parentDevice)
+ return;
+
+ QList deviceDescriptors;
+ foreach(Sonos::GroupObject groupObject, groupObjects) {
+ Device *groupDevice = myDevices().findByParams(ParamList() << Param(sonosGroupDeviceGroupIdParamTypeId, groupObject.groupId));
+ if (groupDevice) {
+ if (groupDevice->name() != groupObject.displayName) {
+ qDebug(dcSonos()) << "Updating group name" << groupDevice->name() << "to" << groupObject.displayName;
+ groupDevice->setName(groupObject.displayName);
+ }
+ } else {
+ //new device, add to the system
+ DeviceDescriptor deviceDescriptor(sonosGroupDeviceClassId, groupObject.displayName, "Sonos Group", parentDevice->id());
+ ParamList params;
+ params.append(Param(sonosGroupDeviceGroupIdParamTypeId, groupObject.groupId));
+ params.append(Param(sonosGroupDeviceHouseholdIdParamTypeId, householdId));
+ deviceDescriptor.setParams(params);
+ deviceDescriptors.append(deviceDescriptor);
+ }
+ }
+
+ if (!deviceDescriptors.isEmpty())
+ emit autoDevicesAppeared(deviceDescriptors);
+
+ //delete auto devices
+ foreach(Device *groupDevice, myDevices().filterByParentDeviceId(parentDevice->id())) {
+ QString groupId = groupDevice->paramValue(sonosGroupDeviceGroupIdParamTypeId).toString();
+ bool deviceRemoved = true;
+ foreach (Sonos::GroupObject groupObject, groupObjects) {
+ if(groupObject.groupId == groupId) {
+ deviceRemoved = false;
+ }
+ }
+ if (deviceRemoved) {
+ emit autoDeviceDisappeared(groupDevice->id());
+ }
+ }
+
+}
+
+void DevicePluginSonos::onPlayBackStatusReceived(const QString &groupId, Sonos::PlayBackObject playBack)
+{
+ Device *device = myDevices().findByParams(ParamList() << Param(sonosGroupDeviceGroupIdParamTypeId, groupId));
+ if (!device)
+ return;
+
+ device->setStateValue(sonosGroupShuffleStateTypeId, playBack.playMode.shuffle);
+
+ if (playBack.playMode.repeatOne) {
+ device->setStateValue(sonosGroupRepeatStateTypeId, "One");
+ } else if (playBack.playMode.repeat) {
+ device->setStateValue(sonosGroupRepeatStateTypeId, "All");
+ } else {
+ device->setStateValue(sonosGroupRepeatStateTypeId, "None");
+ }
+
+ switch (playBack.playbackState) {
+ case Sonos::PlayBackStateIdle:
+ device->setStateValue(sonosGroupPlaybackStatusStateTypeId, "Stopped");
+ break;
+ case Sonos::PlayBackStatePause:
+ device->setStateValue(sonosGroupPlaybackStatusStateTypeId, "Paused");
+ break;
+ case Sonos::PlayBackStateBuffering:
+ case Sonos::PlayBackStatePlaying:
+ device->setStateValue(sonosGroupPlaybackStatusStateTypeId, "Playing");
+ break;
+ }
+}
+
+void DevicePluginSonos::onMetadataStatusReceived(const QString &groupId, Sonos::MetadataStatus metaDataStatus)
+{
+ Device *device = myDevices().findByParams(ParamList() << Param(sonosGroupDeviceGroupIdParamTypeId, groupId));
+ if (!device)
+ return;
+
+ device->setStateValue(sonosGroupTitleStateTypeId, metaDataStatus.currentItem.track.name);
+ device->setStateValue(sonosGroupArtistStateTypeId, metaDataStatus.currentItem.track.artist.name);
+ device->setStateValue(sonosGroupCollectionStateTypeId, metaDataStatus.currentItem.track.album.name);
+ if (!metaDataStatus.currentItem.track.imageUrl.isEmpty()){
+ device->setStateValue(sonosGroupArtworkStateTypeId, metaDataStatus.currentItem.track.imageUrl);
+ } else {
+ device->setStateValue(sonosGroupArtworkStateTypeId, metaDataStatus.container.imageUrl);
+ }
+}
+
+void DevicePluginSonos::onVolumeReceived(const QString &groupId, Sonos::VolumeObject groupVolume)
+{
+ Device *device = myDevices().findByParams(ParamList() << Param(sonosGroupDeviceGroupIdParamTypeId, groupId));
+ if (!device)
+ return;
+
+ device->setStateValue(sonosGroupVolumeStateTypeId, groupVolume.volume);
+ device->setStateValue(sonosGroupMuteStateTypeId, groupVolume.muted);
+}
+
+void DevicePluginSonos::onActionExecuted(QUuid sonosActionId, bool success)
+{
+ if (m_pendingActions.contains(sonosActionId)) {
+ QPointer info = m_pendingActions.value(sonosActionId);
+ if (info.isNull()) {
+ qCWarning(dcSonos()) << "DeviceActionInfo has disappeared. Did it time out?";
+ return;
+ }
+ if (success) {
+ info->finish(Device::DeviceErrorNoError);
+ } else {
+ info->finish(Device::DeviceErrorHardwareFailure);
+ }
+ }
+
+ if (m_pendingBrowserExecution.contains(sonosActionId)) {
+ BrowserActionInfo *info = m_pendingBrowserExecution.value(sonosActionId);
+ if (!info) {
+ qCWarning(dcSonos()) << "BrowseActionInfo has disappeared. Did it time out?";
+ return;
+ }
+
+ if (success) {
+ info->finish(Device::DeviceErrorNoError);
+ } else {
+ info->finish(Device::DeviceErrorHardwareFailure);
+ }
+ }
+}
diff --git a/sonos/devicepluginsonos.h b/sonos/devicepluginsonos.h
new file mode 100644
index 00000000..a23cf3f7
--- /dev/null
+++ b/sonos/devicepluginsonos.h
@@ -0,0 +1,90 @@
+/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+ * *
+ * Copyright (C) 2019 Bernhard Trinnes . *
+ * *
+ * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
+
+#ifndef DEVICEPLUGINSONOS_H
+#define DEVICEPLUGINSONOS_H
+
+#include "devices/deviceplugin.h"
+#include "plugintimer.h"
+#include "sonos.h"
+
+#include
+#include
+
+class DevicePluginSonos : public DevicePlugin
+{
+ Q_OBJECT
+ Q_PLUGIN_METADATA(IID "io.nymea.DevicePlugin" FILE "devicepluginsonos.json")
+ Q_INTERFACES(DevicePlugin)
+
+public:
+ explicit DevicePluginSonos();
+ ~DevicePluginSonos() override;
+
+ void setupDevice(DeviceSetupInfo *info) override;
+ void startPairing(DevicePairingInfo *info) override;
+ void confirmPairing(DevicePairingInfo *info, const QString &username, const QString &secret) override;
+
+ void postSetupDevice(Device *device) override;
+ void startMonitoringAutoDevices() override;
+ void deviceRemoved(Device *device) override;
+ void executeAction(DeviceActionInfo *info) override;
+
+ void browseDevice(BrowseResult *result) override;
+ void browserItem(BrowserItemResult *result) override;
+ void executeBrowserItem(BrowserActionInfo *info) override;
+
+private:
+ PluginTimer *m_pluginTimer5sec = nullptr;
+ PluginTimer *m_pluginTimer60sec = nullptr;
+
+ QHash m_setupSonosConnections;
+ QHash m_sonosConnections;
+ QList m_householdIds;
+
+ QByteArray m_sonosConnectionAccessToken;
+ QByteArray m_sonosConnectionRefreshToken;
+
+ QHash > m_pendingActions;
+ QHash m_pendingBrowseResult;
+ QHash m_pendingBrowserItemResult;
+ QHash m_pendingBrowserExecution;
+
+ const QString m_browseFavoritesPrefix = "/favorites";
+
+private slots:
+ void onConnectionChanged(bool connected);
+ void onAuthenticationStatusChanged(bool authenticated);
+
+ void onHouseholdIdsReceived(QList householdIds);
+ void onFavoritesReceived(QUuid requestId, const QString &householdId, QList favorites);
+ void onPlaylistsReceived(const QString &householdId, QList playlists);
+ void onPlaylistSummaryReceived(const QString &householdId, Sonos::PlaylistSummaryObject playlistSummary);
+
+ void onGroupsReceived(const QString &householdId, QList groupIds);
+ void onPlayBackStatusReceived(const QString &groupId, Sonos::PlayBackObject playBack);
+ void onMetadataStatusReceived(const QString &groupId, Sonos::MetadataStatus metaDataStatus);
+ void onVolumeReceived(const QString &groupId, Sonos::VolumeObject groupVolume);
+ void onActionExecuted(QUuid actionId, bool success);
+};
+
+#endif // DEVICEPLUGINSONOS_H
diff --git a/sonos/devicepluginsonos.json b/sonos/devicepluginsonos.json
new file mode 100644
index 00000000..c357fd9c
--- /dev/null
+++ b/sonos/devicepluginsonos.json
@@ -0,0 +1,223 @@
+{
+ "id": "cdb07719-c445-4fa5-9c7a-564ee02a4412",
+ "name": "Sonos",
+ "displayName": "Sonos",
+ "vendors": [
+ {
+
+ "id": "30a60752-d06f-4ec9-a4e1-9810a5d22fa3",
+ "name": "sonos",
+ "displayName": "Sonos",
+ "deviceClasses": [
+ {
+ "id": "22df416d-7732-44f1-b6b9-e41296211178",
+ "name": "sonosConnection",
+ "displayName": "Sonos connection",
+ "interfaces": ["account", "gateway"],
+ "createMethods": ["user"],
+ "setupMethod": "oauth",
+ "paramTypes": [
+ ],
+ "stateTypes": [
+ {
+ "id": "5aa4360c-61de-47d0-a72e-a19d57712e1c",
+ "name": "connected",
+ "displayName": "Connected",
+ "displayNameEvent": "Connected changed",
+ "defaultValue": false,
+ "type": "bool",
+ "cached": false
+ },
+ {
+ "id": "48b5c1bf-7df0-45d0-9ba3-290fc3acddc3",
+ "name": "loggedIn",
+ "displayName": "Logged in",
+ "displayNameEvent": "Logged in changed",
+ "defaultValue": true,
+ "type": "bool"
+ },
+ {
+ "id": "fb993eab-f1b5-44dd-9b99-041faec5a3b9",
+ "name": "userDisplayName",
+ "displayName": "User name",
+ "displayNameEvent": "User name changed",
+ "type": "QString",
+ "defaultValue": ""
+ }
+ ]
+ },
+ {
+ "id": "72d9332b-2b25-4136-87a6-e534eae4cc80",
+ "name": "sonosGroup",
+ "displayName": "Sonos group",
+ "interfaces": ["extendedvolumecontroller", "mediametadataprovider", "shufflerepeat", "connectable"],
+ "createMethods": ["auto"],
+ "browsable": true,
+ "paramTypes": [
+ {
+ "id": "defc44cd-2ffb-4af1-b348-d6a3474c7515",
+ "name": "groupId",
+ "displayName": "Group id",
+ "type" : "QString"
+ },
+ {
+ "id": "f8a5d3d8-fad9-441d-b345-3484524490a0",
+ "name": "householdId",
+ "displayName": "Household id",
+ "type" : "QString"
+ }
+ ],
+ "stateTypes": [
+ {
+ "id": "09dfbd40-c97c-4a20-9ecd-f80e389a4864",
+ "name": "connected",
+ "displayName": "Connected",
+ "displayNameEvent": "Connected changed",
+ "defaultValue": false,
+ "type": "bool"
+ },
+ {
+ "id": "bc98cdb0-4d0e-48ca-afc7-922e49bb7813",
+ "name": "mute",
+ "displayName": "Mute",
+ "displayNameEvent": "Mute changed",
+ "displayNameAction": "Set mute",
+ "type": "bool",
+ "defaultValue": true,
+ "writable": true
+ },
+ {
+ "id": "9dfe5d78-4c3f-497c-bab1-bb9fdf7e93a9",
+ "name": "volume",
+ "displayName": "Volume",
+ "displayNameEvent": "Volume changed",
+ "displayNameAction": "Set volume",
+ "unit": "Percentage",
+ "type": "int",
+ "minValue": 0,
+ "maxValue": 100,
+ "defaultValue": 50,
+ "writable": true
+ },
+ {
+ "id": "2dd512b7-40c2-488e-8d4f-6519edaa6f74",
+ "name": "playbackStatus",
+ "displayName": "Playback status",
+ "type": "QString",
+ "possibleValues": ["Playing", "Paused", "Stopped"],
+ "defaultValue": "Stopped",
+ "displayNameEvent": "Playback status changed",
+ "displayNameAction": "Set playback status",
+ "writable": true
+ },
+ {
+ "id": "f2209fec-cceb-46ad-8189-4caf42166e6b",
+ "type": "QString",
+ "name": "title",
+ "displayName": "Title",
+ "displayNameEvent": "Title changed",
+ "defaultValue": ""
+ },
+ {
+ "id": "8cb920a3-3bf1-4231-92d4-8ac27e7b3d65",
+ "type": "QString",
+ "name": "artist",
+ "displayName": "Artist",
+ "displayNameEvent": "Artist changed",
+ "defaultValue": ""
+ },
+ {
+ "id": "ce399eec-9f6a-4903-9916-0e90e38b255e",
+ "type": "QString",
+ "name": "collection",
+ "displayName": "Collection",
+ "displayNameEvent": "Collection changed",
+ "defaultValue": ""
+ },
+ {
+ "id": "44304c82-c2f6-433b-b62b-815382617d0b",
+ "type": "QString",
+ "name": "artwork",
+ "displayName": "Artwork",
+ "displayNameEvent": "Artwork changed",
+ "defaultValue": ""
+ },
+ {
+ "id": "5913aa2a-629d-4de5-bf44-a4a1f130c118",
+ "type": "bool",
+ "name": "shuffle",
+ "displayName": "Shuffle",
+ "displayNameEvent": "Shuffle changed",
+ "displayNameAction": "Set shuffle",
+ "defaultValue": false,
+ "writable": true
+ },
+ {
+ "id": "bc02c28e-3f5d-4de4-b9b5-c0b1576c6e7e",
+ "type": "QString",
+ "name": "repeat",
+ "displayName": "Repeat",
+ "displayNameEvent": "Repeat changed",
+ "displayNameAction": "Set repeat",
+ "possibleValues": ["None", "One", "All"],
+ "defaultValue": "None",
+ "writable": true
+ }
+ ],
+ "eventTypes": [
+ {
+ "id": "2535a1eb-7643-4874-98f6-b027fdff6311",
+ "name": "onPlayerPlay",
+ "displayName": "Group play"
+ },
+ {
+ "id": "99498b1c-e9c0-480a-9e91-662ee79ba976",
+ "name": "onPlayerPause",
+ "displayName": "Group pause"
+ },
+ {
+ "id": "a02ce255-3abb-435d-a92e-7f99c952ecb2",
+ "name": "onPlayerStop",
+ "displayName": "Group stop"
+ }
+ ],
+ "actionTypes": [
+ {
+ "id": "a180807d-1265-4831-9d86-a421767418dd",
+ "name": "skipBack",
+ "displayName": "Skip back"
+ },
+ {
+ "id": "7e70b47b-7e79-4521-be34-04a3c427e5b1",
+ "name": "fastRewind",
+ "displayName": "Rewind"
+ },
+ {
+ "id": "ae3cbe03-ee3e-410e-abbd-efabc2402198",
+ "name": "stop",
+ "displayName": "Stop"
+ },
+ {
+ "id": "4d2ee668-a2e3-4795-8b96-0c800b703b46",
+ "name": "play",
+ "displayName": "Play"
+ },
+ {
+ "id": "3cf341cb-fe63-40bc-a450-9678d18e91e3",
+ "name": "pause",
+ "displayName": "Pause"
+ },
+ {
+ "id": "85d7126a-b123-4a28-aeb4-d84bcfb4d14f",
+ "name": "skipNext",
+ "displayName": "Skip Next"
+ }
+ ],
+ "browserItemActionTypes": [
+ ]
+ }
+ ]
+ }
+ ]
+}
+
diff --git a/sonos/sonos.cpp b/sonos/sonos.cpp
new file mode 100644
index 00000000..30f9a857
--- /dev/null
+++ b/sonos/sonos.cpp
@@ -0,0 +1,1623 @@
+/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+ * *
+ * Copyright (C) 2019 Bernhard Trinnes *
+ * *
+ * This file is part of nymea. *
+ * *
+ * This library is free software; you can redistribute it and/or *
+ * modify it under the terms of the GNU Lesser General Public *
+ * License as published by the Free Software Foundation; either *
+ * version 2.1 of the License, or (at your option) any later version. *
+ * *
+ * This library is distributed in the hope that it will be useful, *
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of *
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU *
+ * Lesser General Public License for more details. *
+ * *
+ * You should have received a copy of the GNU Lesser General Public *
+ * License along with this library; If not, see *
+ * . *
+ * *
+ * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
+
+#include "sonos.h"
+#include "extern-plugininfo.h"
+
+#include
+#include
+#include
+#include
+
+Sonos::Sonos(NetworkAccessManager *networkmanager, const QByteArray &clientKey, const QByteArray &clientSecret, QObject *parent) :
+ QObject(parent),
+ m_clientKey(clientKey),
+ m_clientSecret(clientSecret),
+ m_networkManager(networkmanager)
+{
+ if(!m_tokenRefreshTimer) {
+ m_tokenRefreshTimer = new QTimer(this);
+ m_tokenRefreshTimer->setSingleShot(true);
+ connect(m_tokenRefreshTimer, &QTimer::timeout, this, &Sonos::onRefreshTimeout);
+ }
+}
+
+QUrl Sonos::getLoginUrl(const QUrl &redirectUrl)
+{
+ if (m_clientKey.isEmpty()) {
+ qWarning(dcSonos()) << "Client key not defined!";
+ return QUrl("");
+ }
+
+ if (redirectUrl.isEmpty()){
+ qWarning(dcSonos()) << "No redirect uri defined!";
+ }
+ m_redirectUri = QUrl::toPercentEncoding(redirectUrl.toString());
+
+ QUrl url("https://api.sonos.com/login/v3/oauth");
+ QUrlQuery queryParams;
+ queryParams.addQueryItem("client_id", m_clientKey);
+ queryParams.addQueryItem("redirect_uri", m_redirectUri);
+ queryParams.addQueryItem("response_type", "code");
+ queryParams.addQueryItem("scope", "playback-control-all");
+ queryParams.addQueryItem("state", QUuid::createUuid().toString());
+ url.setQuery(queryParams);
+
+ return url;
+}
+
+QByteArray Sonos::accessToken()
+{
+ return m_accessToken;
+}
+
+QByteArray Sonos::refreshToken()
+{
+ return m_refreshToken;
+}
+
+void Sonos::getHouseholds()
+{
+ QNetworkRequest request;
+ request.setHeader(QNetworkRequest::KnownHeaders::ContentTypeHeader, "application/json");
+ request.setRawHeader("Authorization", "Bearer " + m_accessToken);
+ request.setRawHeader("X-Sonos-Api-Key", m_clientKey);
+ request.setUrl(QUrl(m_baseControlUrl + "/households"));
+ QNetworkReply *reply = m_networkManager->get(request);
+ qDebug(dcSonos()) << "Sending request" << request.url() << request.rawHeaderList() << request.rawHeader("Authorization");
+ connect(reply, &QNetworkReply::finished, this, [reply, this] {
+ reply->deleteLater();
+ int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
+
+ // Check HTTP status code
+ if (status != 200 || reply->error() != QNetworkReply::NoError) {
+ if (reply->error() == QNetworkReply::HostNotFoundError) {
+ emit connectionChanged(false);
+ }
+ if (status == 400 || status == 401) {
+ emit authenticationStatusChanged(false);
+ }
+ qCWarning(dcSonos()) << "Request error:" << status << reply->errorString();
+ return;
+ }
+ emit connectionChanged(true);
+ emit authenticationStatusChanged(true);
+
+ QJsonParseError error;
+ QJsonDocument data = QJsonDocument::fromJson(reply->readAll(), &error);
+ if (error.error != QJsonParseError::NoError) {
+ qDebug(dcSonos()) << "Household ID: Recieved invalide JSON object";
+ return;
+ }
+ QList households;
+ foreach (const QVariant &variant, data.toVariant().toMap().value("households").toList()) {
+ QVariantMap obj = variant.toMap();
+ //qDebug(dcSonos()) << "Household ID received:" << obj["id"].toString();
+ households.append(obj["id"].toString());
+ }
+ emit householdIdsReceived(households);
+ });
+}
+
+
+QUuid Sonos::loadFavorite(const QString &groupId, const QString &favouriteId)
+{
+ QNetworkRequest request;
+ request.setHeader(QNetworkRequest::KnownHeaders::ContentTypeHeader, "application/json");
+ request.setRawHeader("Authorization", "Bearer " + m_accessToken);
+ request.setRawHeader("X-Sonos-Api-Key", m_clientKey);
+ request.setUrl(QUrl(m_baseControlUrl + "/groups/" + groupId + "/favorites"));
+ QUuid actionId = QUuid::createUuid();
+
+ QJsonObject object;
+ object.insert("favoriteId", favouriteId);
+ object.insert("playOnCompletion", true);
+ QJsonDocument doc(object);
+ qDebug(dcSonos()) << "Sending request" << doc.toJson();
+
+ QNetworkReply *reply = m_networkManager->post(request, doc.toJson(QJsonDocument::Compact));
+ connect(reply, &QNetworkReply::finished, this, [reply, actionId, this] {
+ reply->deleteLater();
+ int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
+
+ // Check HTTP status code
+ if (status != 200 || reply->error() != QNetworkReply::NoError) {
+ if (reply->error() == QNetworkReply::HostNotFoundError) {
+ emit connectionChanged(false);
+ }
+ if (status == 400 || status == 401) {
+ emit authenticationStatusChanged(false);
+ }
+ emit actionExecuted(actionId, false);
+ qCWarning(dcSonos()) << "Request error:" << status << reply->errorString();
+ return;
+ }
+ emit connectionChanged(true);
+ emit authenticationStatusChanged(true);
+ emit actionExecuted(actionId, true);
+ });
+ return actionId;
+}
+
+QUuid Sonos::getFavorites(const QString &householdId)
+{
+ QNetworkRequest request;
+ request.setHeader(QNetworkRequest::KnownHeaders::ContentTypeHeader, "application/json");
+ request.setRawHeader("Authorization", "Bearer " + m_accessToken);
+ request.setRawHeader("X-Sonos-Api-Key", m_clientKey);
+ request.setUrl(QUrl(m_baseControlUrl + "/households/" + householdId + "/favorites"));
+ QUuid requestId = QUuid::createUuid();
+
+ QNetworkReply *reply = m_networkManager->get(request);
+ connect(reply, &QNetworkReply::finished, this, [reply, requestId, householdId, this] {
+ reply->deleteLater();
+ int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
+
+ // Check HTTP status code
+ if (status != 200 || reply->error() != QNetworkReply::NoError) {
+ if (reply->error() == QNetworkReply::HostNotFoundError) {
+ emit connectionChanged(false);
+ }
+ if (status == 400 || status == 401) {
+ emit authenticationStatusChanged(false);
+ }
+ qCWarning(dcSonos()) << "Request error:" << status << reply->errorString();
+ return;
+ }
+ emit connectionChanged(true);
+ emit authenticationStatusChanged(true);
+
+ QJsonParseError error;
+ QJsonDocument data = QJsonDocument::fromJson(reply->readAll(), &error);
+ if (error.error != QJsonParseError::NoError) {
+ qCWarning(dcSonos()) << "Invalid json received from server";
+ return;
+ }
+
+ if (!data.toVariant().toMap().contains("items"))
+ return;
+
+ QVariantList array = data.toVariant().toMap().value("items").toList();
+ //qDebug(dcSonos()) << "Favorites received:" << data.toJson();
+
+ QList favorites;
+ foreach (const QVariant &variant, array) {
+ QVariantMap itemObject = variant.toMap();
+ FavoriteObject favorite;
+ favorite.id = itemObject["id"].toString();
+ favorite.name = itemObject["name"].toString();
+ favorite.description = itemObject["description"].toString();
+ favorite.imageUrl = itemObject["imageUrl"].toString();
+ favorites.append(favorite);
+ }
+ emit favoritesReceived(requestId, householdId, favorites);
+ });
+ return requestId;
+}
+
+
+void Sonos::getGroups(const QString &householdId)
+{
+ QNetworkRequest request;
+ request.setHeader(QNetworkRequest::KnownHeaders::ContentTypeHeader, "application/json");
+ request.setRawHeader("Authorization", "Bearer " + m_accessToken);
+ request.setRawHeader("X-Sonos-Api-Key", m_clientKey);
+ request.setUrl(QUrl(m_baseControlUrl + "/households/" + householdId + "/groups"));
+ QNetworkReply *reply = m_networkManager->get(request);
+ connect(reply, &QNetworkReply::finished, this, [reply, householdId, this] {
+ reply->deleteLater();
+ int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
+
+ // Check HTTP status code
+ if (status != 200 || reply->error() != QNetworkReply::NoError) {
+ if (reply->error() == QNetworkReply::HostNotFoundError) {
+ emit connectionChanged(false);
+ }
+ if (status == 400 || status == 401) {
+ emit authenticationStatusChanged(false);
+ }
+ qCWarning(dcSonos()) << "Request error:" << status << reply->errorString();
+ return;
+ }
+ emit connectionChanged(true);
+ emit authenticationStatusChanged(true);
+
+ //qDebug(dcSonos()) << "Received response from Sonos" << reply->readAll();
+ QJsonParseError error;
+ QJsonDocument data = QJsonDocument::fromJson(reply->readAll(), &error);
+ if (error.error != QJsonParseError::NoError)
+ return;
+
+ if (!data.toVariant().toMap().contains("groups"))
+ return;
+
+ QVariantList array = data.toVariant().toMap().value("groups").toList();
+ QList groupObjects;
+ foreach (const QVariant &value, array) {
+ QVariantMap obj = value.toMap();
+ //qDebug(dcSonos()) << "Group ID received:" << obj["id"].toString();
+ GroupObject group;
+ group.groupId = obj["id"].toString();
+ group.displayName = obj["name"].toString();
+ group.CoordinatorId = obj["coordinatorId"].toString();
+ group.playbackState = obj["playbackState"].toString();
+ QVariantList players = obj.value("playerIds").toList();
+ foreach (const QVariant &value, players) {
+ group.playerIds.append(value.toByteArray());
+ }
+ groupObjects.append(group);
+ }
+ emit groupsReceived(householdId, groupObjects);
+ });
+}
+
+void Sonos::getGroupVolume(const QString &groupId)
+{
+ QNetworkRequest request;
+ request.setHeader(QNetworkRequest::KnownHeaders::ContentTypeHeader, "application/json");
+ request.setRawHeader("Authorization", "Bearer " + m_accessToken);
+ request.setRawHeader("X-Sonos-Api-Key", m_clientKey);
+ request.setUrl(QUrl(m_baseControlUrl + "/groups/" + groupId + "/groupVolume"));
+ QNetworkReply *reply = m_networkManager->get(request);
+ connect(reply, &QNetworkReply::finished, this, [reply, groupId, this] {
+ reply->deleteLater();
+ int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
+
+ // Check HTTP status code
+ if (status != 200 || reply->error() != QNetworkReply::NoError) {
+ if (reply->error() == QNetworkReply::HostNotFoundError) {
+ emit connectionChanged(false);
+ }
+ if (status == 400 || status == 401) {
+ emit authenticationStatusChanged(false);
+ }
+ qCWarning(dcSonos()) << "Request error:" << status << reply->errorString();
+ return;
+ }
+ emit connectionChanged(true);
+ emit authenticationStatusChanged(true);
+
+ //qDebug(dcSonos()) << "Received response from Sonos" << reply->readAll();
+ QJsonParseError error;
+ QJsonDocument data = QJsonDocument::fromJson(reply->readAll(), &error);
+ if (error.error != QJsonParseError::NoError) {
+ qCWarning(dcSonos()) << "JSON Parse error" << error.errorString();
+ return;
+ }
+
+ VolumeObject volume;
+
+ QVariantMap variant = data.toVariant().toMap();
+ volume.volume = variant["volume"].toInt();
+ volume.muted = variant["muted"].toBool();
+ volume.fixed = variant["fixed"].toBool();
+
+ emit volumeReceived(groupId, volume);
+ });
+}
+
+QUuid Sonos::setGroupVolume(const QString &groupId, int volume)
+{
+ QNetworkRequest request;
+ request.setHeader(QNetworkRequest::KnownHeaders::ContentTypeHeader, "application/json");
+ request.setRawHeader("Authorization", "Bearer " + m_accessToken);
+ request.setRawHeader("X-Sonos-Api-Key", m_clientKey);
+ request.setUrl(QUrl(m_baseControlUrl + "/groups/" + groupId + "/groupVolume"));
+ QUuid actionId = QUuid::createUuid();
+
+ QJsonObject object;
+ object.insert("volume", volume);
+ QJsonDocument doc(object);
+ qDebug(dcSonos()) << "Set volume:" << groupId << doc.toJson(QJsonDocument::Compact);
+
+ QNetworkReply *reply = m_networkManager->post(request, doc.toJson(QJsonDocument::Compact));
+ connect(reply, &QNetworkReply::finished, this, [reply, actionId, groupId, this] {
+ reply->deleteLater();
+ int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
+
+ // Check HTTP status code
+ if (status != 200 || reply->error() != QNetworkReply::NoError) {
+ if (reply->error() == QNetworkReply::HostNotFoundError) {
+ emit connectionChanged(false);
+ }
+ if (status == 400 || status == 401) {
+ emit authenticationStatusChanged(false);
+ }
+ emit actionExecuted(actionId, false);
+ qCWarning(dcSonos()) << "Request error:" << status << reply->errorString();
+ return;
+ }
+ emit connectionChanged(true);
+ emit authenticationStatusChanged(true);
+ emit actionExecuted(actionId, true);
+ getGroupVolume(groupId);
+ });
+ return actionId;
+}
+
+QUuid Sonos::setGroupMute(const QString &groupId, bool mute)
+{
+ QNetworkRequest request;
+ request.setHeader(QNetworkRequest::KnownHeaders::ContentTypeHeader, "application/json");
+ request.setRawHeader("Authorization", "Bearer " + m_accessToken);
+ request.setRawHeader("X-Sonos-Api-Key", m_clientKey);
+ request.setUrl(QUrl(m_baseControlUrl + "/groups/" + groupId + "/groupVolume/mute"));
+ QUuid actionId = QUuid::createUuid();
+
+ QJsonObject object;
+ object.insert("muted", mute);
+ QJsonDocument doc(object);
+
+ qDebug(dcSonos()) << "Set mute:" << groupId << doc.toJson(QJsonDocument::Compact);
+
+ QNetworkReply *reply = m_networkManager->post(request, doc.toJson(QJsonDocument::Compact));
+ connect(reply, &QNetworkReply::finished, this, [reply, actionId, groupId, this] {
+ reply->deleteLater();
+ int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
+
+ // Check HTTP status code
+ if (status != 200 || reply->error() != QNetworkReply::NoError) {
+ if (reply->error() == QNetworkReply::HostNotFoundError) {
+ emit connectionChanged(false);
+ }
+ if (status == 400 || status == 401) {
+ emit authenticationStatusChanged(false);
+ }
+ emit actionExecuted(actionId, false);
+ qCWarning(dcSonos()) << "Request error:" << status << reply->errorString();
+ return;
+ }
+ emit connectionChanged(true);
+ emit authenticationStatusChanged(true);
+ emit actionExecuted(actionId, true);
+ getGroupVolume(groupId);
+ });
+ return actionId;
+}
+
+QUuid Sonos::setGroupRelativeVolume(const QString &groupId, int volumeDelta)
+{
+ QNetworkRequest request;
+ request.setHeader(QNetworkRequest::KnownHeaders::ContentTypeHeader, "application/json");
+ request.setRawHeader("Authorization", "Bearer " + m_accessToken);
+ request.setRawHeader("X-Sonos-Api-Key", m_clientKey);
+ request.setUrl(QUrl(m_baseControlUrl + "/groups/" + groupId + "/groupVolume/relative"));
+ QUuid actionId = QUuid::createUuid();
+
+ QJsonObject object;
+ object.insert("volumeDelta", QJsonValue::fromVariant(volumeDelta));
+ QJsonDocument doc(object);
+
+ qDebug(dcSonos()) << "Relative volume:" << groupId << volumeDelta;
+
+ QNetworkReply *reply = m_networkManager->post(request, doc.toJson(QJsonDocument::Compact));
+ connect(reply, &QNetworkReply::finished, this, [reply, actionId, groupId, this] {
+ reply->deleteLater();
+ int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
+
+ // Check HTTP status code
+ if (status != 200 || reply->error() != QNetworkReply::NoError) {
+ if (reply->error() == QNetworkReply::HostNotFoundError) {
+ emit connectionChanged(false);
+ }
+ if (status == 400 || status == 401) {
+ emit authenticationStatusChanged(false);
+ }
+ emit actionExecuted(actionId, false);
+ qCWarning(dcSonos()) << "Request error:" << status << reply->errorString();
+ return;
+ }
+ emit connectionChanged(true);
+ emit authenticationStatusChanged(true);
+ emit actionExecuted(actionId, true);
+ getGroupVolume(groupId);
+ });
+ return actionId;
+}
+
+void Sonos::getGroupPlaybackStatus(const QString &groupId)
+{
+ QNetworkRequest request;
+ request.setHeader(QNetworkRequest::KnownHeaders::ContentTypeHeader, "application/json");
+ request.setRawHeader("Authorization", "Bearer " + m_accessToken);
+ request.setRawHeader("X-Sonos-Api-Key", m_clientKey);
+ request.setUrl(QUrl(m_baseControlUrl + "/groups/" + groupId + "/playback"));
+ QNetworkReply *reply = m_networkManager->get(request);
+ connect(reply, &QNetworkReply::finished, this, [reply, this, groupId] {
+ reply->deleteLater();
+ int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
+
+ // Check HTTP status code
+ if (status != 200 || reply->error() != QNetworkReply::NoError) {
+ if (reply->error() == QNetworkReply::HostNotFoundError) {
+ emit connectionChanged(false);
+ }
+ if (status == 400 || status == 401) {
+ emit authenticationStatusChanged(false);
+ }
+ qCWarning(dcSonos()) << "Request error:" << status << reply->errorString();
+ return;
+ }
+ emit connectionChanged(true);
+ emit authenticationStatusChanged(true);
+
+ QJsonDocument data = QJsonDocument::fromJson(reply->readAll());
+ if (!data.isObject())
+ return;
+
+ PlayBackObject playBack;
+ QJsonObject object = data.object();
+ playBack.itemId = object["itemId"].toString();
+ playBack.positionMillis = object["positionMillis"].toInt();
+ playBack.previousItemId = object["previousItemId"].toInt();
+ playBack.previousPositionMillis = object["previousPositionMillis"].toInt();
+ QString playBackState = object["playbackState"].toString();
+ if (playBackState.contains("BUFFERING")) {
+ playBack.playbackState = PlayBackStateBuffering;
+ } else if (playBackState.contains("IDLE")) {
+ playBack.playbackState = PlayBackStateIdle;
+ } else if (playBackState.contains("PAUSE")) {
+ playBack.playbackState = PlayBackStatePause;
+ } else if (playBackState.contains("PLAYING")) {
+ playBack.playbackState = PlayBackStatePlaying;
+ }
+ playBack.isDucking = object["isDucking"].toBool();
+ playBack.queueVersion = object["queueVersion"].toString();
+ if (object.contains("playModes")) {
+ PlayMode playMode;
+ QJsonObject playModeObject = object["playModes"].toObject();
+ playMode.repeat = playModeObject["repeat"].toBool();
+ playMode.repeatOne = playModeObject["repeatOne"].toBool();
+ playMode.crossfade = playModeObject["crossfade"].toBool();
+ playMode.shuffle = playModeObject["shuffle"].toBool();
+ playBack.playMode = playMode;
+ }
+ emit playBackStatusReceived(groupId, playBack);
+ });
+}
+
+QUuid Sonos::groupLoadLineIn(const QString &groupId)
+{
+ qDebug(dcSonos()) << "Load line in:" << groupId;
+ QNetworkRequest request;
+ request.setHeader(QNetworkRequest::KnownHeaders::ContentTypeHeader, "application/json");
+ request.setRawHeader("Authorization", "Bearer " + m_accessToken);
+ request.setRawHeader("X-Sonos-Api-Key", m_clientKey);
+ request.setUrl(QUrl(m_baseControlUrl + "/groups/" + groupId + "/playback/lineIn"));
+ QUuid actionId = QUuid::createUuid();
+
+ QNetworkReply *reply = m_networkManager->post(request, "");
+ connect(reply, &QNetworkReply::finished, this, [reply, actionId, groupId, this] {
+ reply->deleteLater();
+ int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
+
+ // Check HTTP status code
+ if (status != 200 || reply->error() != QNetworkReply::NoError) {
+ if (reply->error() == QNetworkReply::HostNotFoundError) {
+ emit connectionChanged(false);
+ }
+ if (status == 400 || status == 401) {
+ emit authenticationStatusChanged(false);
+ }
+ emit actionExecuted(actionId, false);
+ qCWarning(dcSonos()) << "Request error:" << status << reply->errorString();
+ return;
+ }
+ emit connectionChanged(true);
+ emit authenticationStatusChanged(true);
+ emit actionExecuted(actionId, true);
+ getGroupVolume(groupId);
+ });
+ return actionId;
+}
+
+QUuid Sonos::groupPlay(const QString &groupId)
+{
+ QNetworkRequest request;
+ request.setHeader(QNetworkRequest::KnownHeaders::ContentTypeHeader, "application/json");
+ request.setRawHeader("Authorization", "Bearer " + m_accessToken);
+ request.setRawHeader("X-Sonos-Api-Key", m_clientKey);
+ request.setUrl(QUrl(m_baseControlUrl + "/groups/" + groupId + "/playback/play"));
+ QUuid actionId = QUuid::createUuid();
+
+ qDebug(dcSonos()) << "Play:" << groupId;
+
+ QNetworkReply *reply = m_networkManager->post(request, "");
+ connect(reply, &QNetworkReply::finished, this, [reply, actionId, groupId, this] {
+ reply->deleteLater();
+ int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
+
+ // Check HTTP status code
+ if (status != 200 || reply->error() != QNetworkReply::NoError) {
+ if (reply->error() == QNetworkReply::HostNotFoundError) {
+ emit connectionChanged(false);
+ }
+ if (status == 400 || status == 401) {
+ emit authenticationStatusChanged(false);
+ }
+ emit actionExecuted(actionId, false);
+ qCWarning(dcSonos()) << "Request error:" << status << reply->errorString();
+ return;
+ }
+ emit connectionChanged(true);
+ emit authenticationStatusChanged(true);
+ emit actionExecuted(actionId, true);
+ getGroupPlaybackStatus(groupId);
+ });
+ return actionId;
+}
+
+QUuid Sonos::groupPause(const QString &groupId)
+{
+ QNetworkRequest request;
+ request.setHeader(QNetworkRequest::KnownHeaders::ContentTypeHeader, "application/json");
+ request.setRawHeader("Authorization", "Bearer " + m_accessToken);
+ request.setRawHeader("X-Sonos-Api-Key", m_clientKey);
+ request.setUrl(QUrl(m_baseControlUrl + "/groups/" + groupId + "/playback/pause"));
+ QUuid actionId = QUuid::createUuid();
+
+ qDebug(dcSonos()) << "Pause:" << groupId;
+
+ QNetworkReply *reply = m_networkManager->post(request, "");
+ connect(reply, &QNetworkReply::finished, this, [reply, actionId, groupId, this] {
+ reply->deleteLater();
+ int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
+
+ // Check HTTP status code
+ if (status != 200 || reply->error() != QNetworkReply::NoError) {
+ if (reply->error() == QNetworkReply::HostNotFoundError) {
+ emit connectionChanged(false);
+ }
+ if (status == 400 || status == 401) {
+ emit authenticationStatusChanged(false);
+ }
+ emit actionExecuted(actionId, false);
+ qCWarning(dcSonos()) << "Request error:" << status << reply->errorString();
+ return;
+ }
+ emit connectionChanged(true);
+ emit authenticationStatusChanged(true);
+ emit actionExecuted(actionId, true);
+ getGroupPlaybackStatus(groupId);
+ });
+ return actionId;
+}
+
+QUuid Sonos::groupSeek(const QString &groupId, int possitionMillis)
+{
+ Q_UNUSED(groupId)
+ QNetworkRequest request;
+ request.setHeader(QNetworkRequest::KnownHeaders::ContentTypeHeader, "application/json");
+ request.setRawHeader("Authorization", "Bearer " + m_accessToken);
+ request.setRawHeader("X-Sonos-Api-Key", m_clientKey);
+ request.setUrl(QUrl(m_baseControlUrl + "/groups/" + groupId + "/playback/seek"));
+ QUuid actionId = QUuid::createUuid();
+
+ QJsonObject object;
+ object.insert("positionMillis", QJsonValue::fromVariant(possitionMillis));
+ QJsonDocument doc(object);
+
+ QNetworkReply *reply = m_networkManager->post(request, doc.toJson(QJsonDocument::Compact));
+ connect(reply, &QNetworkReply::finished, this, [reply, actionId, this] {
+ reply->deleteLater();
+ int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
+
+ // Check HTTP status code
+ if (status != 200 || reply->error() != QNetworkReply::NoError) {
+ if (reply->error() == QNetworkReply::HostNotFoundError) {
+ emit connectionChanged(false);
+ }
+ if (status == 400 || status == 401) {
+ emit authenticationStatusChanged(false);
+ }
+ emit actionExecuted(actionId, false);
+ qCWarning(dcSonos()) << "Request error:" << status << reply->errorString();
+ return;
+ }
+ emit connectionChanged(true);
+ emit authenticationStatusChanged(true);
+ emit actionExecuted(actionId, true);
+ });
+ return actionId;
+}
+
+QUuid Sonos::groupSeekRelative(const QString &groupId, int deltaMillis)
+{
+ QNetworkRequest request;
+ request.setHeader(QNetworkRequest::KnownHeaders::ContentTypeHeader, "application/json");
+ request.setRawHeader("Authorization", "Bearer " + m_accessToken);
+ request.setRawHeader("X-Sonos-Api-Key", m_clientKey);
+ request.setUrl(QUrl(m_baseControlUrl + "/groups/" + groupId + "/playback/seekRelative"));
+ QUuid actionId = QUuid::createUuid();
+
+ QJsonObject object;
+ object.insert("deltaMillis", QJsonValue::fromVariant(deltaMillis));
+ QJsonDocument doc(object);
+
+ QNetworkReply *reply = m_networkManager->post(request, doc.toJson(QJsonDocument::Compact));
+ connect(reply, &QNetworkReply::finished, this, [reply, actionId, this] {
+ reply->deleteLater();
+ int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
+
+ // Check HTTP status code
+ if (status != 200 || reply->error() != QNetworkReply::NoError) {
+ if (reply->error() == QNetworkReply::HostNotFoundError) {
+ emit connectionChanged(false);
+ }
+ if (status == 400 || status == 401) {
+ emit authenticationStatusChanged(false);
+ }
+ emit actionExecuted(actionId, false);
+ qCWarning(dcSonos()) << "Request error:" << status << reply->errorString();
+ return;
+ }
+ emit connectionChanged(true);
+ emit authenticationStatusChanged(true);
+ emit actionExecuted(actionId, true);
+ });
+ return actionId;
+}
+
+QUuid Sonos::groupSetPlayModes(const QString &groupId, PlayMode playMode)
+{
+ QNetworkRequest request;
+ request.setHeader(QNetworkRequest::KnownHeaders::ContentTypeHeader, "application/json");
+ request.setRawHeader("Authorization", "Bearer " + m_accessToken);
+ request.setRawHeader("X-Sonos-Api-Key", m_clientKey);
+ request.setUrl(QUrl(m_baseControlUrl + "/groups/" + groupId + "/playback/playMode"));
+ QUuid actionId = QUuid::createUuid();
+
+ QJsonObject object;
+ QJsonObject playModesObject;
+ playModesObject["repeat"] = playMode.repeat;
+ playModesObject["repeatOne"] = playMode.repeatOne;
+ playModesObject["crossfade"] = playMode.crossfade;
+ playModesObject["shuffle"] = playMode.shuffle;
+ object.insert("playModes", playModesObject);
+ QJsonDocument doc(object);
+
+ QNetworkReply *reply = m_networkManager->post(request, doc.toJson(QJsonDocument::Compact));
+ connect(reply, &QNetworkReply::finished, this, [reply, actionId, groupId, this] {
+ reply->deleteLater();
+ int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
+
+ // Check HTTP status code
+ if (status != 200 || reply->error() != QNetworkReply::NoError) {
+ if (reply->error() == QNetworkReply::HostNotFoundError) {
+ emit connectionChanged(false);
+ }
+ if (status == 400 || status == 401) {
+ emit authenticationStatusChanged(false);
+ }
+ emit actionExecuted(actionId, false);
+ qCWarning(dcSonos()) << "Request error:" << status << reply->errorString();
+ return;
+ }
+ emit connectionChanged(true);
+ emit authenticationStatusChanged(true);
+ emit actionExecuted(actionId, true);
+ getGroupPlaybackStatus(groupId);
+ });
+ return actionId;
+}
+
+QUuid Sonos::groupSetShuffle(const QString &groupId, bool shuffle)
+{
+ QNetworkRequest request;
+ request.setHeader(QNetworkRequest::KnownHeaders::ContentTypeHeader, "application/json");
+ request.setRawHeader("Authorization", "Bearer " + m_accessToken);
+ request.setRawHeader("X-Sonos-Api-Key", m_clientKey);
+ request.setUrl(QUrl(m_baseControlUrl + "/groups/" + groupId + "/playback/playMode"));
+ QUuid actionId = QUuid::createUuid();
+
+ QJsonObject object;
+ QJsonObject playModesObject;
+ playModesObject["shuffle"] = shuffle;
+ object.insert("playModes", playModesObject);
+ QJsonDocument doc(object);
+
+ QNetworkReply *reply = m_networkManager->post(request, doc.toJson(QJsonDocument::Compact));
+ connect(reply, &QNetworkReply::finished, this, [reply, actionId, groupId, this] {
+ reply->deleteLater();
+ int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
+
+ // Check HTTP status code
+ if (status != 200 || reply->error() != QNetworkReply::NoError) {
+ if (reply->error() == QNetworkReply::HostNotFoundError) {
+ emit connectionChanged(false);
+ }
+ if (status == 400 || status == 401) {
+ emit authenticationStatusChanged(false);
+ }
+ emit actionExecuted(actionId, false);
+ qCWarning(dcSonos()) << "Request error:" << status << reply->errorString();
+ return;
+ }
+ emit connectionChanged(true);
+ emit authenticationStatusChanged(true);
+ emit actionExecuted(actionId, true);
+ getGroupPlaybackStatus(groupId);
+ });
+ return actionId;
+}
+
+QUuid Sonos::groupSetRepeat(const QString &groupId, RepeatMode repeatMode)
+{
+ QNetworkRequest request;
+ request.setHeader(QNetworkRequest::KnownHeaders::ContentTypeHeader, "application/json");
+ request.setRawHeader("Authorization", "Bearer " + m_accessToken);
+ request.setRawHeader("X-Sonos-Api-Key", m_clientKey);
+ request.setUrl(QUrl(m_baseControlUrl + "/groups/" + groupId + "/playback/playMode"));
+ QUuid actionId = QUuid::createUuid();
+
+ QJsonObject object;
+ QJsonObject playModesObject;
+ if (repeatMode == RepeatModeAll) {
+ qDebug(dcSonos()) << "Setting repeat mode all";
+ playModesObject["repeat"] = true;
+ playModesObject["repeatOne"] = false;
+ } else if (repeatMode == RepeatModeOne) {
+ qDebug(dcSonos()) << "Setting repeat mode one";
+ playModesObject["repeat"] = false;
+ playModesObject["repeatOne"] = true;
+ } else if (repeatMode == RepeatModeNone) {
+ qDebug(dcSonos()) << "Setting repeat mode none";
+ playModesObject["repeat"] = false;
+ playModesObject["repeatOne"] = false;
+ }
+ object.insert("playModes", playModesObject);
+ QJsonDocument doc(object);
+
+ QNetworkReply *reply = m_networkManager->post(request, doc.toJson(QJsonDocument::Compact));
+ connect(reply, &QNetworkReply::finished, this, [reply, actionId, groupId, this] {
+ reply->deleteLater();
+ int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
+
+ // Check HTTP status code
+ if (status != 200 || reply->error() != QNetworkReply::NoError) {
+ if (reply->error() == QNetworkReply::HostNotFoundError) {
+ emit connectionChanged(false);
+ }
+ if (status == 400 || status == 401) {
+ emit authenticationStatusChanged(false);
+ }
+ emit actionExecuted(actionId, false);
+ qCWarning(dcSonos()) << "Request error:" << status << reply->errorString();
+ return;
+ }
+ emit connectionChanged(true);
+ emit authenticationStatusChanged(true);
+ emit actionExecuted(actionId, true);
+ getGroupPlaybackStatus(groupId);
+ });
+ return actionId;
+}
+
+QUuid Sonos::groupSetCrossfade(const QString &groupId, bool crossfade)
+{
+ QNetworkRequest request;
+ request.setHeader(QNetworkRequest::KnownHeaders::ContentTypeHeader, "application/json");
+ request.setRawHeader("Authorization", "Bearer " + m_accessToken);
+ request.setRawHeader("X-Sonos-Api-Key", m_clientKey);
+ request.setUrl(QUrl(m_baseControlUrl + "/groups/" + groupId + "/playback/playMode"));
+ QUuid actionId = QUuid::createUuid();
+
+ QJsonObject object;
+ QJsonObject playModesObject;
+ playModesObject["crossfade"] = crossfade;
+ object.insert("playModes", playModesObject);
+ QJsonDocument doc(object);
+
+ QNetworkReply *reply = m_networkManager->post(request, doc.toJson(QJsonDocument::Compact));
+ connect(reply, &QNetworkReply::finished, this, [reply, actionId, groupId, this] {
+ reply->deleteLater();
+ int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
+
+ // Check HTTP status code
+ if (status != 200 || reply->error() != QNetworkReply::NoError) {
+ if (reply->error() == QNetworkReply::HostNotFoundError) {
+ emit connectionChanged(false);
+ }
+ if (status == 400 || status == 401) {
+ emit authenticationStatusChanged(false);
+ }
+ emit actionExecuted(actionId, false);
+ qCWarning(dcSonos()) << "Request error:" << status << reply->errorString();
+ return;
+ }
+ emit connectionChanged(true);
+ emit authenticationStatusChanged(true);
+ emit actionExecuted(actionId, true);
+ getGroupPlaybackStatus(groupId);
+ });
+ return actionId;
+}
+
+QUuid Sonos::groupSkipToNextTrack(const QString &groupId)
+{
+ QNetworkRequest request;
+ request.setHeader(QNetworkRequest::KnownHeaders::ContentTypeHeader, "application/json");
+ request.setRawHeader("Authorization", "Bearer " + m_accessToken);
+ request.setRawHeader("X-Sonos-Api-Key", m_clientKey);
+ request.setUrl(QUrl(m_baseControlUrl + "/groups/" + groupId + "/playback/skipToNextTrack"));
+ QUuid actionId = QUuid::createUuid();
+
+ QNetworkReply *reply = m_networkManager->post(request, "");
+ connect(reply, &QNetworkReply::finished, this, [reply, actionId, groupId, this] {
+ reply->deleteLater();
+ int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
+
+ // Check HTTP status code
+ if (status != 200 || reply->error() != QNetworkReply::NoError) {
+ if (reply->error() == QNetworkReply::HostNotFoundError) {
+ emit connectionChanged(false);
+ }
+ if (status == 400 || status == 401) {
+ emit authenticationStatusChanged(false);
+ }
+ emit actionExecuted(actionId, false);
+ qCWarning(dcSonos()) << "Request error:" << status << reply->errorString();
+ return;
+ }
+ emit connectionChanged(true);
+ emit authenticationStatusChanged(true);
+ emit actionExecuted(actionId, true);
+ getGroupMetadataStatus(groupId);
+ });
+ return actionId;
+}
+
+QUuid Sonos::groupSkipToPreviousTrack(const QString &groupId)
+{
+ QNetworkRequest request;
+ request.setHeader(QNetworkRequest::KnownHeaders::ContentTypeHeader, "application/json");
+ request.setRawHeader("Authorization", "Bearer " + m_accessToken);
+ request.setRawHeader("X-Sonos-Api-Key", m_clientKey);
+ request.setUrl(QUrl(m_baseControlUrl + "/groups/" + groupId + "/playback/skipToPreviousTrack"));
+ QUuid actionId = QUuid::createUuid();
+
+ QNetworkReply *reply = m_networkManager->post(request, "");
+ connect(reply, &QNetworkReply::finished, this, [reply, actionId, groupId, this] {
+ reply->deleteLater();
+ int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
+
+ // Check HTTP status code
+ if (status != 200 || reply->error() != QNetworkReply::NoError) {
+ if (reply->error() == QNetworkReply::HostNotFoundError) {
+ emit connectionChanged(false);
+ }
+ if (status == 400 || status == 401) {
+ emit authenticationStatusChanged(false);
+ }
+ emit actionExecuted(actionId, false);
+ qCWarning(dcSonos()) << "Request error:" << status << reply->errorString();
+ return;
+ }
+ emit connectionChanged(true);
+ emit authenticationStatusChanged(true);
+ emit actionExecuted(actionId, true);
+ getGroupMetadataStatus(groupId);
+ });
+ return actionId;
+}
+
+QUuid Sonos::groupTogglePlayPause(const QString &groupId)
+{
+ QNetworkRequest request;
+ request.setHeader(QNetworkRequest::KnownHeaders::ContentTypeHeader, "application/json");
+ request.setRawHeader("Authorization", "Bearer " + m_accessToken);
+ request.setRawHeader("X-Sonos-Api-Key", m_clientKey);
+ request.setUrl(QUrl(m_baseControlUrl + "/groups/" + groupId + "/playback/togglePlayPause"));
+ QUuid actionId = QUuid::createUuid();
+
+ QNetworkReply *reply = m_networkManager->post(request, "");
+ connect(reply, &QNetworkReply::finished, this, [reply, actionId, groupId, this] {
+ reply->deleteLater();
+ int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
+
+ // Check HTTP status code
+ if (status != 200 || reply->error() != QNetworkReply::NoError) {
+ if (reply->error() == QNetworkReply::HostNotFoundError) {
+ emit connectionChanged(false);
+ }
+ if (status == 400 || status == 401) {
+ emit authenticationStatusChanged(false);
+ }
+ emit actionExecuted(actionId, false);
+ qCWarning(dcSonos()) << "Request error:" << status << reply->errorString();
+ return;
+ }
+ emit connectionChanged(true);
+ emit authenticationStatusChanged(true);
+ emit actionExecuted(actionId, true);
+ getGroupPlaybackStatus(groupId);
+ });
+ return actionId;
+}
+
+void Sonos::getGroupMetadataStatus(const QString &groupId)
+{
+ QNetworkRequest request;
+ request.setHeader(QNetworkRequest::KnownHeaders::ContentTypeHeader, "application/json");
+ request.setRawHeader("Authorization", "Bearer " + m_accessToken);
+ request.setRawHeader("X-Sonos-Api-Key", m_clientKey);
+ request.setUrl(QUrl(m_baseControlUrl + "/groups/" + groupId + "/playbackMetadata"));
+ QNetworkReply *reply = m_networkManager->get(request);
+ connect(reply, &QNetworkReply::finished, this, [reply, groupId, this] {
+ reply->deleteLater();
+ int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
+
+ // Check HTTP status code
+ if (status != 200 || reply->error() != QNetworkReply::NoError) {
+ if (reply->error() == QNetworkReply::HostNotFoundError) {
+ emit connectionChanged(false);
+ }
+ if (status == 400 || status == 401) {
+ emit authenticationStatusChanged(false);
+ }
+ qCWarning(dcSonos()) << "Request error:" << status << reply->errorString();
+ return;
+ }
+ emit connectionChanged(true);
+ emit authenticationStatusChanged(true);
+
+ QJsonDocument data = QJsonDocument::fromJson(reply->readAll());
+ if (!data.isObject())
+ return;
+
+ MetadataStatus metaDataStatus;
+ QJsonObject object = data.object();
+
+ if (object.contains("container")) {
+ ContainerObject container;
+ QJsonObject containerObject = object["container"].toObject();
+ container.name = containerObject["name"].toString();
+ container.type = containerObject["type"].toString();
+ container.imageUrl = containerObject["imageUrl"].toString();
+ if (containerObject.contains("service")) {
+ ServiceObject service;
+ QJsonObject serviceObject = containerObject.value("artist").toObject();
+ service.name = serviceObject["name"].toString();
+ container.service = service;
+ }
+ if (containerObject.contains("id")) {
+ //TODO parse ID
+ }
+ metaDataStatus.container = container;
+ }
+
+ if (object.contains("currentItem")) {
+ QJsonObject currentItemObject = object["currentItem"].toObject();
+ ItemObject currentItem;
+ if (currentItemObject.contains("track")) {
+ TrackObject track;
+ QJsonObject trackObject = currentItemObject["track"].toObject();
+
+ if (trackObject.contains("artist")) {
+ ArtistObject artist;
+ QJsonObject artistObject = trackObject["artist"].toObject();
+ artist.name = artistObject["name"].toString();
+ //qDebug(dcSonos()) << "Track object contains artist" << artist.name;
+ track.artist = artist;
+ }
+ if (trackObject.contains("album")) {
+ AlbumObject album;
+ QJsonObject albumObject = trackObject["album"].toObject();
+ album.name = albumObject["name"].toString();
+ //qDebug(dcSonos()) << "Track object contains album" << album.name;
+ track.album = album;
+ }
+ if (trackObject.contains("service")) {
+ ServiceObject service;
+ QJsonObject serviceObject = trackObject["service"].toObject();
+ service.name = serviceObject["name"].toString();
+ //qDebug(dcSonos()) << "Track object contains service" << service.name;
+ track.service = service;
+ }
+ if (trackObject.contains("id")) {
+ //TODO parse id
+ }
+
+ track.type = trackObject["type"].toString();
+ track.name = trackObject["name"].toString();
+ track.imageUrl = trackObject["imageUrl"].toString();
+ track.trackNumber = trackObject["trackNumber"].toInt();
+ track.durationMillis = trackObject["durationMillis"].toInt();
+
+ currentItem.track = track;
+ }
+ metaDataStatus.currentItem = currentItem;
+ }
+
+ if (object.contains("nextItem")) {
+ ItemObject nextItem;
+ QJsonObject nextItemObject = object["nextItem"].toObject();
+ if (nextItemObject.contains("track")) {
+ TrackObject track;
+ QJsonObject trackObject = nextItemObject.value("track").toObject();
+
+ if (trackObject.contains("artist")) {
+ ArtistObject artist;
+ QJsonObject artistObject = trackObject.value("artist").toObject();
+ artist.name = artistObject["name"].toString();
+ //qDebug(dcSonos()) << "Track object contains artist" << artist.name;
+ track.artist = artist;
+ }
+ if (trackObject.contains("album")) {
+ AlbumObject album;
+ QJsonObject albumObject = trackObject.value("album").toObject();
+ album.name = albumObject["name"].toString();
+ //qDebug(dcSonos()) << "Track object contains album" << album.name;
+ track.album = album;
+ }
+ if (trackObject.contains("service")) {
+ ServiceObject service;
+ QJsonObject serviceObject = trackObject.value("service").toObject();
+ service.name = serviceObject["name"].toString();
+ //qDebug(dcSonos()) << "Track object contains service" << service.name;
+ track.service = service;
+ }
+ if (trackObject.contains("id")) {
+ //TODO parse id
+ }
+ track.type = trackObject["type"].toString();
+ track.name = trackObject["name"].toString();
+ track.imageUrl = trackObject["imageUrl"].toString();
+ track.trackNumber = trackObject["trackNumber"].toInt();
+ track.durationMillis = trackObject["durationMillis"].toInt();
+
+ nextItem.track = track;
+ }
+ metaDataStatus.nextItem = nextItem;
+ }
+ emit metadataStatusReceived(groupId, metaDataStatus);
+ });
+}
+
+void Sonos::getPlayerVolume(const QByteArray &playerId)
+{
+ QNetworkRequest request;
+ request.setHeader(QNetworkRequest::KnownHeaders::ContentTypeHeader, "application/json");
+ request.setRawHeader("Authorization", "Bearer " + m_accessToken);
+ request.setRawHeader("X-Sonos-Api-Key", m_clientKey);
+ request.setUrl(QUrl(m_baseControlUrl + "/players/" + playerId + "/playerVolume"));
+ QNetworkReply *reply = m_networkManager->get(request);
+ connect(reply, &QNetworkReply::finished, this, [reply, playerId, this] {
+ reply->deleteLater();
+ int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
+
+ // Check HTTP status code
+ if (status != 200 || reply->error() != QNetworkReply::NoError) {
+ if (reply->error() == QNetworkReply::HostNotFoundError) {
+ emit connectionChanged(false);
+ }
+ if (status == 400 || status == 401) {
+ emit authenticationStatusChanged(false);
+ }
+ qCWarning(dcSonos()) << "Request error:" << status << reply->errorString();
+ return;
+ }
+ emit connectionChanged(true);
+ emit authenticationStatusChanged(true);
+
+ //qDebug(dcSonos()) << "Received response from Sonos" << reply->readAll();
+ QJsonParseError error;
+ QJsonDocument data = QJsonDocument::fromJson(reply->readAll(), &error);
+ if (error.error != QJsonParseError::NoError) {
+ qCWarning(dcSonos()) << "Json parse error" << error.errorString();
+ return;
+ }
+
+ VolumeObject volume;
+ QVariantMap variant = data.toVariant().toMap();
+ volume.volume = variant["volume"].toInt();
+ volume.muted = variant["muted"].toBool();
+ volume.fixed = variant["fixed"].toBool();
+ emit playerVolumeReceived(playerId, volume);
+ });
+}
+
+QUuid Sonos::setPlayerVolume(const QByteArray &playerId, int volume)
+{
+ QNetworkRequest request;
+ request.setHeader(QNetworkRequest::KnownHeaders::ContentTypeHeader, "application/json");
+ request.setRawHeader("Authorization", "Bearer " + m_accessToken);
+ request.setRawHeader("X-Sonos-Api-Key", m_clientKey);
+ request.setUrl(QUrl(m_baseControlUrl + "/players/" + playerId + "/playerVolume"));
+ QUuid actionId = QUuid::createUuid();
+
+ qDebug(dcSonos()) << "Setting volume:" << playerId << volume;
+
+ QJsonObject object;
+ object.insert("volume", QJsonValue::fromVariant(volume));
+ QJsonDocument doc(object);
+
+ QNetworkReply *reply = m_networkManager->post(request, doc.toJson(QJsonDocument::Compact));
+ connect(reply, &QNetworkReply::finished, this, [reply, actionId, playerId, this] {
+ reply->deleteLater();
+ int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
+
+ // Check HTTP status code
+ if (status != 200 || reply->error() != QNetworkReply::NoError) {
+ if (reply->error() == QNetworkReply::HostNotFoundError) {
+ emit connectionChanged(false);
+ }
+ if (status == 400 || status == 401) {
+ emit authenticationStatusChanged(false);
+ }
+ emit actionExecuted(actionId, false);
+ qCWarning(dcSonos()) << "Request error:" << status << reply->errorString();
+ return;
+ }
+ emit connectionChanged(true);
+ emit authenticationStatusChanged(true);
+ emit actionExecuted(actionId, true);
+ getPlayerVolume(playerId);
+ });
+ return actionId;
+}
+
+QUuid Sonos::setPlayerRelativeVolume(const QByteArray &playerId, int volumeDelta)
+{
+ QNetworkRequest request;
+ request.setHeader(QNetworkRequest::KnownHeaders::ContentTypeHeader, "application/json");
+ request.setRawHeader("Authorization", "Bearer " + m_accessToken);
+ request.setRawHeader("X-Sonos-Api-Key", m_clientKey);
+ request.setUrl(QUrl(m_baseControlUrl + "/players/" + playerId + "/playerVolume/relative"));
+ QUuid actionId = QUuid::createUuid();
+
+ QJsonObject object;
+ object.insert("volumeDelta", QJsonValue::fromVariant(volumeDelta));
+ QJsonDocument doc(object);
+
+ QNetworkReply *reply = m_networkManager->post(request, doc.toJson(QJsonDocument::Compact));
+ connect(reply, &QNetworkReply::finished, this, [reply, actionId, playerId, this] {
+ reply->deleteLater();
+ int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
+
+ // Check HTTP status code
+ if (status != 200 || reply->error() != QNetworkReply::NoError) {
+ if (reply->error() == QNetworkReply::HostNotFoundError) {
+ emit connectionChanged(false);
+ }
+ if (status == 400 || status == 401) {
+ emit authenticationStatusChanged(false);
+ }
+ emit actionExecuted(actionId, false);
+ qCWarning(dcSonos()) << "Request error:" << status << reply->errorString();
+ return;
+ }
+ emit connectionChanged(true);
+ emit authenticationStatusChanged(true);
+ emit actionExecuted(actionId, true);
+ getPlayerVolume(playerId);
+ });
+ return actionId;
+}
+
+QUuid Sonos::setPlayerMute(const QByteArray &playerId, bool mute)
+{
+ QNetworkRequest request;
+ request.setHeader(QNetworkRequest::KnownHeaders::ContentTypeHeader, "application/json");
+ request.setRawHeader("Authorization", "Bearer " + m_accessToken);
+ request.setRawHeader("X-Sonos-Api-Key", m_clientKey);
+ request.setUrl(QUrl(m_baseControlUrl + "/players/" + playerId + "/playerVolume"));
+ QUuid actionId = QUuid::createUuid();
+
+ QJsonObject object;
+ object.insert("muted", QJsonValue::fromVariant(mute));
+ QJsonDocument doc(object);
+
+ QNetworkReply *reply = m_networkManager->post(request, doc.toJson(QJsonDocument::Compact));
+ connect(reply, &QNetworkReply::finished, this, [reply, actionId, playerId, this] {
+ reply->deleteLater();
+ int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
+
+ // Check HTTP status code
+ if (status != 200 || reply->error() != QNetworkReply::NoError) {
+ if (reply->error() == QNetworkReply::HostNotFoundError) {
+ emit connectionChanged(false);
+ }
+ if (status == 400 || status == 401) {
+ emit authenticationStatusChanged(false);
+ }
+ emit actionExecuted(actionId, false);
+ qCWarning(dcSonos()) << "Request error:" << status << reply->errorString();
+ return;
+ }
+ emit connectionChanged(true);
+ emit authenticationStatusChanged(true);
+ emit actionExecuted(actionId, true);
+ getPlayerVolume(playerId);
+ });
+ return actionId;
+}
+
+void Sonos::getPlaylist(const QString &householdId, const QString &playlistId)
+{
+ QNetworkRequest request;
+ request.setHeader(QNetworkRequest::KnownHeaders::ContentTypeHeader, "application/json");
+ request.setRawHeader("Authorization", "Bearer " + m_accessToken);
+ request.setRawHeader("X-Sonos-Api-Key", m_clientKey);
+ request.setUrl(QUrl(m_baseControlUrl + "/households/" + householdId + "/playlists/getPlaylist"));
+
+
+ QJsonObject object;
+ object["playlistId"] = playlistId;
+ QJsonDocument doc(object);
+
+ QNetworkReply *reply = m_networkManager->post(request, doc.toJson(QJsonDocument::Compact));
+ connect(reply, &QNetworkReply::finished, this, [reply, householdId, this] {
+ reply->deleteLater();
+ int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
+
+ // Check HTTP status code
+ if (status != 200 || reply->error() != QNetworkReply::NoError) {
+ if (reply->error() == QNetworkReply::HostNotFoundError) {
+ emit connectionChanged(false);
+ }
+ if (status == 400 || status == 401) {
+ emit authenticationStatusChanged(false);
+ }
+ qCWarning(dcSonos()) << "Request error:" << status << reply->errorString();
+ return;
+ }
+ emit connectionChanged(true);
+ emit authenticationStatusChanged(true);
+
+ //qDebug(dcSonos()) << "Received response from Sonos" << reply->readAll();
+ QJsonParseError error;
+ QJsonDocument data = QJsonDocument::fromJson(reply->readAll(), &error);
+ if (error.error != QJsonParseError::NoError) {
+ qCWarning(dcSonos()) << "Json parse error" << error.errorString();
+ return;
+ }
+
+ QVariantMap variant = data.toVariant().toMap();
+
+ if (!variant.contains("tracks"))
+ return;
+
+ PlaylistSummaryObject playlist;
+ QVariantList array = variant["tracks"].toList();
+ foreach (const QVariant &value, array) {
+ QVariantMap itemObject = value.toMap();
+ PlaylistTrackObject track;
+ track.name = itemObject["name"].toString();
+ track.album = itemObject["album"].toString();
+ track.artist= itemObject["artist"].toString();
+ playlist.tracks.append(track);
+ }
+ emit playlistSummaryReceived(householdId, playlist);
+ });
+}
+
+void Sonos::getPlaylists(const QString &householdId)
+{
+ QNetworkRequest request;
+ request.setHeader(QNetworkRequest::KnownHeaders::ContentTypeHeader, "application/json");
+ request.setRawHeader("Authorization", "Bearer " + m_accessToken);
+ request.setRawHeader("X-Sonos-Api-Key", m_clientKey);
+ request.setUrl(QUrl(m_baseControlUrl + "/households/" + householdId + "/playlists"));
+ QNetworkReply *reply = m_networkManager->get(request);
+ connect(reply, &QNetworkReply::finished, this, [reply, householdId, this] {
+ reply->deleteLater();
+ int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
+
+ // Check HTTP status code
+ if (status != 200 || reply->error() != QNetworkReply::NoError) {
+ if (reply->error() == QNetworkReply::HostNotFoundError) {
+ emit connectionChanged(false);
+ }
+ if (status == 400 || status == 401) {
+ emit authenticationStatusChanged(false);
+ }
+ qCWarning(dcSonos()) << "Request error:" << status << reply->errorString();
+ return;
+ }
+ emit connectionChanged(true);
+ emit authenticationStatusChanged(true);
+
+ //qDebug(dcSonos()) << "Received response from Sonos" << reply->readAll();
+ QJsonParseError error;
+ QJsonDocument data = QJsonDocument::fromJson(reply->readAll(), &error);
+ if (!data.isObject()) {
+ qCWarning(dcSonos()) << "Json parse error:" << error.errorString();
+ return;
+ }
+
+ if (!data.toVariant().toMap().contains("playlists"))
+ return;
+
+ QVariantList array = data.toVariant().toMap().value("playlists").toList();
+ QList playlists;
+ foreach (const QVariant &value, array) {
+ QVariantMap itemObject = value.toMap();
+ PlaylistObject playlist;
+ playlist.id = itemObject["id"].toString();
+ playlist.name = itemObject["name"].toString();
+ playlist.type = itemObject["type"].toString();
+ playlist.trackCount = itemObject["trackCount"].toString();
+ playlists.append(playlist);
+ }
+ emit playlistsReceived(householdId, playlists);
+ });
+}
+
+QUuid Sonos::loadPlaylist(const QString &groupId, const QString &playlistId)
+{
+ QNetworkRequest request;
+ request.setHeader(QNetworkRequest::KnownHeaders::ContentTypeHeader, "application/json");
+ request.setRawHeader("Authorization", "Bearer " + m_accessToken);
+ request.setRawHeader("X-Sonos-Api-Key", m_clientKey);
+ request.setUrl(QUrl(m_baseControlUrl + "/groups/" + groupId + "/playlists"));
+ QUuid actionId = QUuid::createUuid();
+
+ QJsonObject object;
+ object["playlistId"] = playlistId;
+ QJsonDocument doc(object);
+
+ QNetworkReply *reply = m_networkManager->post(request, doc.toJson(QJsonDocument::Compact));
+ connect(reply, &QNetworkReply::finished, this, [reply, actionId, this] {
+ reply->deleteLater();
+ int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
+
+ // Check HTTP status code
+ if (status != 200 || reply->error() != QNetworkReply::NoError) {
+ if (reply->error() == QNetworkReply::HostNotFoundError) {
+ emit connectionChanged(false);
+ }
+ if (status == 400 || status == 401) {
+ emit authenticationStatusChanged(false);
+ }
+ emit actionExecuted(actionId, false);
+ qCWarning(dcSonos()) << "Request error:" << status << reply->errorString();
+ return;
+ }
+ emit connectionChanged(true);
+ emit authenticationStatusChanged(true);
+ emit actionExecuted(actionId, true);
+ });
+ return actionId;
+}
+
+void Sonos::getPlayerSettings(const QString &playerId)
+{
+ QNetworkRequest request;
+ request.setHeader(QNetworkRequest::KnownHeaders::ContentTypeHeader, "application/json");
+ request.setRawHeader("Authorization", "Bearer " + m_accessToken);
+ request.setRawHeader("X-Sonos-Api-Key", m_clientKey);
+ request.setUrl(QUrl(m_baseControlUrl + "/players/" + playerId + "/settings/player"));
+ QNetworkReply *reply = m_networkManager->get(request);
+ connect(reply, &QNetworkReply::finished, this, [reply, playerId, this] {
+ reply->deleteLater();
+ int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
+
+ // Check HTTP status code
+ if (status != 200 || reply->error() != QNetworkReply::NoError) {
+ if (reply->error() == QNetworkReply::HostNotFoundError) {
+ emit connectionChanged(false);
+ }
+ if (status == 400 || status == 401) {
+ emit authenticationStatusChanged(false);
+ }
+ qCWarning(dcSonos()) << "Request error:" << status << reply->errorString();
+ return;
+ }
+ emit connectionChanged(true);
+ emit authenticationStatusChanged(true);
+
+ QJsonParseError error;
+ QJsonDocument jsonDoc = QJsonDocument::fromJson(reply->readAll(), &error);
+ if (error.error != QJsonParseError::NoError) {
+ qCWarning(dcSonos()) << "Json parse error" << error.errorString();
+ return;
+ }
+
+ QVariantMap data = jsonDoc.toVariant().toMap();
+ PlayerSettingsObject playerSettings;
+ playerSettings.monoMode = data["monoMode"].toBool();
+ playerSettings.volumeMode = data["volumeMode"].toString();
+ playerSettings.wifiDisabled = data["wifiDisable"].toBool();
+ playerSettings.volumeScalingFactor = data["wifiDisable"].toDouble();
+ emit playerSettingsRecieved(playerId, playerSettings);
+ });
+}
+
+QUuid Sonos::setPlayerSettings(const QString &playerId, PlayerSettingsObject settings)
+{
+ QNetworkRequest request;
+ request.setHeader(QNetworkRequest::KnownHeaders::ContentTypeHeader, "application/json");
+ request.setRawHeader("Authorization", "Bearer " + m_accessToken);
+ request.setRawHeader("X-Sonos-Api-Key", m_clientKey);
+ request.setUrl(QUrl(m_baseControlUrl + "/players/" + playerId + "/settings/player"));
+ QUuid actionId = QUuid::createUuid();
+
+ QJsonObject object;
+ object["volumeMode"] = settings.volumeMode;
+ object["volumeScalingFactor"] = settings.volumeScalingFactor;
+ object["monoMode"] = settings.monoMode;
+ object["wifiDisable"] = settings.wifiDisabled;
+ QJsonDocument doc(object);
+
+ QNetworkReply *reply = m_networkManager->post(request, doc.toJson(QJsonDocument::Compact));
+ connect(reply, &QNetworkReply::finished, this, [reply, actionId, playerId, this] {
+ reply->deleteLater();
+ int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
+
+ // Check HTTP status code
+ if (status != 200 || reply->error() != QNetworkReply::NoError) {
+ if (reply->error() == QNetworkReply::HostNotFoundError) {
+ emit connectionChanged(false);
+ }
+ if (status == 400 || status == 401) {
+ emit authenticationStatusChanged(false);
+ }
+ emit actionExecuted(actionId, false);
+ qCWarning(dcSonos()) << "Request error:" << status << reply->errorString();
+ return;
+ }
+ emit connectionChanged(true);
+ emit authenticationStatusChanged(true);
+ emit actionExecuted(actionId, true);
+ getPlayerSettings(playerId);
+ });
+ return actionId;
+}
+
+void Sonos::onRefreshTimeout()
+{
+ qCDebug(dcSonos) << "Refresh authentication token";
+ getAccessTokenFromRefreshToken(m_refreshToken);
+}
+
+
+void Sonos::getAccessTokenFromRefreshToken(const QByteArray &refreshToken)
+{
+ if (refreshToken.isEmpty()) {
+ qWarning(dcSonos()) << "No refresh token given!";
+ emit authenticationStatusChanged(false);
+ return;
+ }
+
+ QUrl url(m_baseAuthorizationUrl);
+ QUrlQuery query;
+ query.clear();
+ query.addQueryItem("grant_type", "refresh_token");
+ query.addQueryItem("refresh_token", refreshToken);
+ url.setQuery(query);
+
+ QNetworkRequest request(url);
+ request.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded; charset=UTF-8");
+
+ QByteArray auth = QByteArray(m_clientKey + ':' + m_clientSecret).toBase64(QByteArray::Base64Encoding | QByteArray::KeepTrailingEquals);
+ request.setRawHeader("Authorization", QString("Basic %1").arg(QString(auth)).toUtf8());
+
+ QNetworkReply *reply = m_networkManager->post(request, QByteArray());
+ connect(reply, &QNetworkReply::finished, this, [this, reply](){
+ reply->deleteLater();
+
+ QJsonDocument jsonDoc = QJsonDocument::fromJson(reply->readAll());
+ int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
+ if (status != 200 || reply->error() != QNetworkReply::NoError) {
+ if(jsonDoc.toVariant().toMap().contains("error_description")) {
+ qWarning(dcSonos()) << "Access token error:" << jsonDoc.toVariant().toMap().value("error_description").toString();
+ }
+ emit authenticationStatusChanged(false);
+ return;
+ }
+ if(!jsonDoc.toVariant().toMap().contains("access_token")) {
+ emit authenticationStatusChanged(false);
+ return;
+ }
+ m_accessToken = jsonDoc.toVariant().toMap().value("access_token").toByteArray();
+
+ if (jsonDoc.toVariant().toMap().contains("expires_in")) {
+ int expireTime = jsonDoc.toVariant().toMap().value("expires_in").toInt();
+ qCDebug(dcSonos()) << "Access token expires at" << QDateTime::currentDateTime().addSecs(expireTime).toString();
+ if (!m_tokenRefreshTimer) {
+ qWarning(dcSonos()) << "Access token refresh timer not initialized";
+ return;
+ }
+ m_tokenRefreshTimer->start((expireTime - 20) * 1000);
+ }
+ emit authenticationStatusChanged(true);;
+ });
+}
+
+void Sonos::getAccessTokenFromAuthorizationCode(const QByteArray &authorizationCode)
+{
+ // Obtaining access token
+ if(authorizationCode.isEmpty())
+ qWarning(dcSonos) << "No auhtorization code given!";
+ if(m_clientKey.isEmpty())
+ qWarning(dcSonos) << "Client key not set!";
+ if(m_clientSecret.isEmpty())
+ qWarning(dcSonos) << "Client secret not set!";
+
+ QUrl url = QUrl(m_baseAuthorizationUrl);
+ QUrlQuery query;
+ query.clear();
+ query.addQueryItem("grant_type", "authorization_code");
+ query.addQueryItem("code", authorizationCode);
+ query.addQueryItem("redirect_uri", m_redirectUri);
+ url.setQuery(query);
+
+ QNetworkRequest request(url);
+ request.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded;charset=utf-8");
+
+ QByteArray auth = QByteArray(m_clientKey + ':' + m_clientSecret).toBase64(QByteArray::Base64Encoding | QByteArray::KeepTrailingEquals);
+ request.setRawHeader("Authorization", QString("Basic %1").arg(QString(auth)).toUtf8());
+
+ QNetworkReply *reply = m_networkManager->post(request, QByteArray());
+ connect(reply, &QNetworkReply::finished, this, [this, reply](){
+ reply->deleteLater();
+ QJsonDocument jsonDoc = QJsonDocument::fromJson(reply->readAll());
+
+ int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
+ switch (status){
+ case 400:
+ if(!jsonDoc.toVariant().toMap().contains("error")) {
+ if(jsonDoc.toVariant().toMap().value("error").toString() == "invalid_client") {
+ qWarning(dcSonos()) << "Client token provided doesn’t correspond to client that generated auth code.";
+ }
+ if(jsonDoc.toVariant().toMap().value("error").toString() == "invalid_redirect_uri") {
+ qWarning(dcSonos()) << "Missing redirect_uri parameter.";
+ }
+ if(jsonDoc.toVariant().toMap().value("error").toString() == "invalid_code") {
+ qWarning(dcSonos()) << "Expired authorization code.";
+ }
+ }
+ return;
+ case 401:
+ qWarning(dcSonos()) << "Client does not have permission to use this API.";
+ return;
+ case 405:
+ qWarning(dcSonos()) << "Wrong HTTP method used.";
+ return;
+ default:
+ break;
+ }
+ qCDebug(dcSonos()) << "Sonos accessToken reply:" << this << reply->error() << reply->errorString() << jsonDoc.toJson();
+ if(!jsonDoc.toVariant().toMap().contains("access_token") || !jsonDoc.toVariant().toMap().contains("refresh_token") ) {
+ emit authenticationStatusChanged(false);
+ return;
+ }
+ qCDebug(dcSonos()) << "Access token:" << jsonDoc.toVariant().toMap().value("access_token").toString();
+ m_accessToken = jsonDoc.toVariant().toMap().value("access_token").toByteArray();
+
+ qCDebug(dcSonos()) << "Refresh token:" << jsonDoc.toVariant().toMap().value("refresh_token").toString();
+ m_refreshToken = jsonDoc.toVariant().toMap().value("refresh_token").toByteArray();
+
+ if (jsonDoc.toVariant().toMap().contains("expires_in")) {
+ int expireTime = jsonDoc.toVariant().toMap().value("expires_in").toInt();
+ qCDebug(dcSonos()) << "expires at" << QDateTime::currentDateTime().addSecs(expireTime).toString();
+ if (!m_tokenRefreshTimer) {
+ qWarning(dcSonos()) << "Token refresh timer not initialized";
+ emit authenticationStatusChanged(false);
+ return;
+ }
+ m_tokenRefreshTimer->start((expireTime - 20) * 1000);
+ }
+ emit authenticationStatusChanged(true);
+ });
+}
diff --git a/sonos/sonos.h b/sonos/sonos.h
new file mode 100644
index 00000000..cb21b678
--- /dev/null
+++ b/sonos/sonos.h
@@ -0,0 +1,299 @@
+/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+ * *
+ * Copyright (C) 2019 Bernhard Trinnes *
+ * *
+ * This file is part of nymea. *
+ * *
+ * This library is free software; you can redistribute it and/or *
+ * modify it under the terms of the GNU Lesser General Public *
+ * License as published by the Free Software Foundation; either *
+ * version 2.1 of the License, or (at your option) any later version. *
+ * *
+ * This library is distributed in the hope that it will be useful, *
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of *
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU *
+ * Lesser General Public License for more details. *
+ * *
+ * You should have received a copy of the GNU Lesser General Public *
+ * License along with this library; If not, see *
+ * . *
+ * *
+ * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
+
+#ifndef SONOS_H
+#define SONOS_H
+
+#include
+#include
+
+#include "network/networkaccessmanager.h"
+#include "devices/device.h"
+
+class Sonos : public QObject
+{
+ Q_OBJECT
+public:
+
+ enum RepeatMode {
+ RepeatModeOne,
+ RepeatModeAll,
+ RepeatModeNone
+ };
+
+ enum PlayBackState {
+ PlayBackStateBuffering,
+ PlayBackStateIdle,
+ PlayBackStatePause,
+ PlayBackStatePlaying
+ };
+
+ struct PlayMode {
+ bool repeat;
+ bool repeatOne;
+ bool shuffle;
+ bool crossfade;
+ };
+
+ /* Represents a Sonos household.*/
+ struct GroupObject {
+ QString CoordinatorId; //Player acting as the group coordinator for the group
+ QString groupId; //The ID of the group.
+ QString playbackState; //The playback state corresponding to the group.
+ QList playerIds; //The IDs of the primary players in the group.
+ QString displayName; //The display name for the group, such as “Living Room” or “Kitchen + 2”.
+ };
+
+ struct VolumeObject {
+ int volume; //Group volume as an integer between 0 and 100, inclusive.
+ bool muted; //A value indicating whether or not the group is muted
+ bool fixed; //A value indicating whether or not the group volume is fixed or changeable.
+ };
+
+ /*
+ * The music service identifier or a pseudo-service identifier in the case of local library. */
+ struct ServiceObject
+ {
+ QString id;
+ QString name;
+ QString imageUrl;
+ };
+
+ /*
+ * Describes a Sonos favorite in the household.
+ * You can see favorites in the My Sonos tab in the app. The following are not considered */
+ struct FavoriteObject {
+ QString id;
+ QString name;
+ QString description;
+ QString imageUrl;
+ ServiceObject service;
+ };
+
+ struct PlaylistObject
+ {
+ QString id;
+ QString name;
+ QString type;
+ QString trackCount;
+ };
+
+ struct PlayerSettingsObject
+ {
+ QString volumeMode;
+ double volumeScalingFactor;
+ bool monoMode;
+ bool wifiDisabled;
+ };
+
+
+ /* The music object identifier for the item in a music service.
+ * This identifies the content within a music service, the music service,
+ * and the account associated with the content. */
+ struct MusicObjectId {
+ QString serviceId;
+ QString objectId;
+ QString accountId;
+ };
+
+ struct TrackPoliciesObject {
+ bool canCrossfade;
+ bool canResume;
+ bool canSeek;
+ bool canSkip;
+ bool canSkipBack;
+ bool canSkipToItem;
+ bool isVisible;
+ };
+
+ /* The artist of the track. */
+ struct ArtistObject
+ {
+ QString name;
+ QString imageUrl;
+ MusicObjectId id;
+ // tags enum
+ };
+
+ struct AlbumObject
+ {
+ QString name;
+ ArtistObject artist;
+ };
+
+ struct ContainerObject
+ {
+ QString name;
+ QString type;
+ MusicObjectId id;
+ ServiceObject service;
+ QString imageUrl;
+ //tags enum
+ };
+
+ struct PlayBackObject {
+ QString itemId;
+ bool isDucking;
+ PlayBackState playbackState;
+ PlayMode playMode;
+ uint positionMillis;
+ QString previousItemId;
+ uint previousPositionMillis;
+ QString queueVersion;
+ };
+
+ /*
+ * A single music track or audio file. Tracks are identified by type,
+ * which determines the key values for the object types included.
+ * The following fields are shared by all types of tracks. */
+ struct TrackObject {
+ QString type;
+ QString name;
+ QString imageUrl;
+ int trackNumber;
+ bool canCrossfade;
+ bool canSkip;
+ int durationMillis;
+ ArtistObject artist;
+ AlbumObject album;
+ ServiceObject service;
+ };
+
+ /* An item in a queue. Used for cloud queue tracks and radio stations that have track-like data
+ * for the currently playing content. For example, the currentItem and nextItem parameters in the
+ * metadataStatus event are item object types.*/
+ struct ItemObject {
+ QString itemId;
+ TrackObject track;
+ bool deleted;
+ TrackPoliciesObject policies;
+ };
+
+ struct MetadataStatus {
+ ContainerObject container;
+ ItemObject currentItem;
+ ItemObject nextItem;
+ };
+
+ struct PlaylistTrackObject {
+ QString name;
+ QString artist;
+ QString album;
+ };
+
+ struct PlaylistSummaryObject {
+ QString id;
+ QString name;
+ QString type;
+ QList tracks;
+ };
+
+ explicit Sonos(NetworkAccessManager *networkManager, const QByteArray &clientId, const QByteArray &clientSecret, QObject *parent = nullptr);
+
+ QUrl getLoginUrl(const QUrl &redirectUrl);
+ QByteArray accessToken();
+ QByteArray refreshToken();
+ void getAccessTokenFromRefreshToken(const QByteArray &refreshToken);
+ void getAccessTokenFromAuthorizationCode(const QByteArray &authorizationCode);
+
+ void getHouseholds();
+ QUuid getFavorites(const QString &householdId);
+ void getGroups(const QString &householdId);
+
+ QUuid loadFavorite(const QString &groupId, const QString &faveriteId);
+
+ //Group volume
+ void getGroupVolume(const QString &groupId); //Get the volume and mute state of a group.
+ //Group volume actions
+ QUuid setGroupVolume(const QString &groupId, int volume); //Set group volume to a specific level and unmute the group if muted.
+ QUuid setGroupMute(const QString &groupId, bool mute); //Mute and unmute the group.
+ QUuid setGroupRelativeVolume(const QString &groupId, int volumeDelta); //Increase or decrease group volume.
+
+ //group playback
+ void getGroupPlaybackStatus(const QString &groupId);
+
+ //Group playback actions
+ QUuid groupLoadLineIn(const QString &groupId);
+ QUuid groupPlay(const QString &groupId);
+ QUuid groupPause(const QString &groupId);
+ QUuid groupSeek(const QString &groupId, int possitionMillis);
+ QUuid groupSeekRelative(const QString &groupId, int deltaMillis);
+ QUuid groupSetPlayModes(const QString &groupId, PlayMode playMode);
+ QUuid groupSetShuffle(const QString &groupId, bool shuffle);
+ QUuid groupSetRepeat(const QString &groupId, RepeatMode repeatMode);
+ QUuid groupSetCrossfade(const QString &groupId, bool crossfade);
+ QUuid groupSkipToNextTrack(const QString &groupId);
+ QUuid groupSkipToPreviousTrack(const QString &groupId);
+ QUuid groupTogglePlayPause(const QString &groupId);
+
+ //playbackMetadata
+ void getGroupMetadataStatus(const QString &groupId);
+
+ // playerVolume
+ void getPlayerVolume(const QByteArray &playerId);
+ QUuid setPlayerVolume(const QByteArray &playerId, int volume);
+ QUuid setPlayerRelativeVolume(const QByteArray &playerId, int volumeDelta);
+ QUuid setPlayerMute(const QByteArray &playerId, bool mute);
+
+ //Playlists API namespace
+ void getPlaylists(const QString &householdId);
+ void getPlaylist(const QString &householdId, const QString &playlistId);
+ QUuid loadPlaylist(const QString &groupId, const QString &playlistId);
+
+ //Settings
+ void getPlayerSettings(const QString &playerId);
+ QUuid setPlayerSettings(const QString &playerId, PlayerSettingsObject settings);
+
+private:
+ QByteArray m_baseAuthorizationUrl = "https://api.sonos.com/login/v3/oauth/access";
+ QByteArray m_baseControlUrl = "https://api.ws.sonos.com/control/api/v1";
+ QByteArray m_clientKey;
+ QByteArray m_clientSecret;
+
+ QByteArray m_accessToken;
+ QByteArray m_refreshToken;
+ QByteArray m_redirectUri;
+
+ NetworkAccessManager *m_networkManager = nullptr;
+ QTimer *m_tokenRefreshTimer = nullptr;
+private slots:
+ void onRefreshTimeout();
+
+signals:
+ void connectionChanged(bool connected);
+ void authenticationStatusChanged(bool authenticated);
+
+ void householdIdsReceived(QList householdIds);
+ void favoritesReceived(QUuid requestId, const QString &householdId, QList favorites);
+ void playlistsReceived(const QString &householdId, QList playlists);
+ void groupsReceived(const QString &householdId, QList groups);
+ void playlistSummaryReceived(const QString &householdId, PlaylistSummaryObject playlistSummary);
+
+ void playBackStatusReceived(const QString &groupId, PlayBackObject playBack);
+ void metadataStatusReceived(const QString &groupId, MetadataStatus metaDataStatus);
+ void volumeReceived(const QString &groupId, VolumeObject groupVolume);
+
+ void playerVolumeReceived(const QString &playerId, VolumeObject playerVolume);
+ void playerSettingsRecieved(const QString &playerId, PlayerSettingsObject playerSettings);
+ void actionExecuted(QUuid actionId, bool success);
+};
+#endif // SONOS_H
diff --git a/sonos/sonos.pro b/sonos/sonos.pro
new file mode 100644
index 00000000..ee83ccea
--- /dev/null
+++ b/sonos/sonos.pro
@@ -0,0 +1,13 @@
+include(../plugins.pri)
+
+QT += network
+
+TARGET = $$qtLibraryTarget(nymea_devicepluginsonos)
+
+SOURCES += \
+ devicepluginsonos.cpp \
+ sonos.cpp \
+
+HEADERS += \
+ devicepluginsonos.h \
+ sonos.h \