Merge PR #149: New plugin: Sonos

master
Jenkins nymea 2019-10-22 01:09:53 +02:00
commit 3039a1b8ef
9 changed files with 2920 additions and 0 deletions

17
debian/control vendored
View File

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

1
debian/nymea-plugin-sonos.install.in vendored Normal file
View File

@ -0,0 +1 @@
usr/lib/@DEB_HOST_MULTIARCH@/nymea/plugins/libnymea_devicepluginsonos.so

View File

@ -41,6 +41,7 @@ PLUGIN_DIRS = \
serialportcommander \
simulation \
snapd \
sonos \
tasmota \
tcpcommander \
texasinstruments \

653
sonos/devicepluginsonos.cpp Normal file
View File

@ -0,0 +1,653 @@
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
* *
* Copyright (C) 2019 Bernhard Trinnes <bernhard.trinnes@nymea.io *
* *
* 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 *
* <http://www.gnu.org/licenses/>. *
* *
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
#include "devicepluginsonos.h"
#include "devices/device.h"
#include "network/networkaccessmanager.h"
#include "plugininfo.h"
#include "types/mediabrowseritem.h"
#include <QNetworkRequest>
#include <QNetworkReply>
#include <QUrlQuery>
#include <QJsonDocument>
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<DeviceActionInfo>(info));
return;
}
if (action.actionTypeId() == sonosGroupShuffleActionTypeId) {
bool shuffle = action.param(sonosGroupShuffleActionShuffleParamTypeId).value().toBool();
m_pendingActions.insert(sonos->groupSetShuffle(groupId, shuffle), QPointer<DeviceActionInfo>(info));
return;
}
if (action.actionTypeId() == sonosGroupRepeatActionTypeId) {
if (action.param(sonosGroupRepeatActionRepeatParamTypeId).value().toString() == "None") {
m_pendingActions.insert(sonos->groupSetRepeat(groupId, Sonos::RepeatModeNone), QPointer<DeviceActionInfo>(info));
} else if (action.param(sonosGroupRepeatActionRepeatParamTypeId).value().toString() == "One") {
m_pendingActions.insert(sonos->groupSetRepeat(groupId, Sonos::RepeatModeOne), QPointer<DeviceActionInfo>(info));
} else if (action.param(sonosGroupRepeatActionRepeatParamTypeId).value().toString() == "All") {
m_pendingActions.insert(sonos->groupSetRepeat(groupId, Sonos::RepeatModeAll), QPointer<DeviceActionInfo>(info));
} else {
return info->finish(Device::DeviceErrorHardwareFailure);
}
return;
}
if (action.actionTypeId() == sonosGroupPauseActionTypeId) {
m_pendingActions.insert(sonos->groupPause(groupId), QPointer<DeviceActionInfo>(info));
return;
}
if (action.actionTypeId() == sonosGroupStopActionTypeId) {
m_pendingActions.insert(sonos->groupPause(groupId), QPointer<DeviceActionInfo>(info));
return;
}
if (action.actionTypeId() == sonosGroupMuteActionTypeId) {
bool mute = action.param(sonosGroupMuteActionMuteParamTypeId).value().toBool();
m_pendingActions.insert(sonos->setGroupMute(groupId, mute), QPointer<DeviceActionInfo>(info));
return;
}
if (action.actionTypeId() == sonosGroupVolumeActionTypeId) {
int volume = action.param(sonosGroupVolumeActionVolumeParamTypeId).value().toInt();
m_pendingActions.insert(sonos->setGroupVolume(groupId, volume), QPointer<DeviceActionInfo>(info));
return;
}
if (action.actionTypeId() == sonosGroupSkipNextActionTypeId) {
m_pendingActions.insert(sonos->groupSkipToNextTrack(groupId), QPointer<DeviceActionInfo>(info));
return;
}
if (action.actionTypeId() == sonosGroupSkipBackActionTypeId) {
m_pendingActions.insert(sonos->groupSkipToPreviousTrack(groupId), QPointer<DeviceActionInfo>(info));
return;
}
if (action.actionTypeId() == sonosGroupPlaybackStatusActionTypeId) {
QString playbackStatus = action.param(sonosGroupPlaybackStatusActionPlaybackStatusParamTypeId).value().toString();
if (playbackStatus == "Playing") {
m_pendingActions.insert(sonos->groupPlay(groupId), QPointer<DeviceActionInfo>(info));
} else if(playbackStatus == "Stopped") {
m_pendingActions.insert(sonos->groupPause(groupId), QPointer<DeviceActionInfo>(info));
} else if(playbackStatus == "Paused") {
m_pendingActions.insert(sonos->groupPause(groupId), QPointer<DeviceActionInfo>(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<Sonos *>(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<Sonos *>(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<QString> householdIds)
{
Sonos *sonos = static_cast<Sonos *>(sender());
foreach(QString householdId, householdIds) {
sonos->getGroups(householdId);
sonos->getPlaylists(householdId);
}
}
void DevicePluginSonos::onFavoritesReceived(QUuid requestId, const QString &householdId, QList<Sonos::FavoriteObject> 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<Sonos::PlaylistObject> playlists)
{
Sonos *sonos = static_cast<Sonos *>(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<Sonos::GroupObject> groupObjects)
{
Sonos *sonos = static_cast<Sonos *>(sender());
Device *parentDevice = m_sonosConnections.key(sonos);
if (!parentDevice)
return;
QList<DeviceDescriptor> 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<DeviceActionInfo> 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);
}
}
}

90
sonos/devicepluginsonos.h Normal file
View File

@ -0,0 +1,90 @@
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
* *
* Copyright (C) 2019 Bernhard Trinnes <bernhard.trinnes@nymea.io *
* *
* 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 *
* <http://www.gnu.org/licenses/>. *
* *
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
#ifndef DEVICEPLUGINSONOS_H
#define DEVICEPLUGINSONOS_H
#include "devices/deviceplugin.h"
#include "plugintimer.h"
#include "sonos.h"
#include <QHash>
#include <QDebug>
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<DeviceId, Sonos *> m_setupSonosConnections;
QHash<Device *, Sonos *> m_sonosConnections;
QList<QByteArray> m_householdIds;
QByteArray m_sonosConnectionAccessToken;
QByteArray m_sonosConnectionRefreshToken;
QHash<QUuid, QPointer<DeviceActionInfo> > m_pendingActions;
QHash<QUuid, BrowseResult *> m_pendingBrowseResult;
QHash<QUuid, BrowserItemResult *> m_pendingBrowserItemResult;
QHash<QUuid, BrowserActionInfo *> m_pendingBrowserExecution;
const QString m_browseFavoritesPrefix = "/favorites";
private slots:
void onConnectionChanged(bool connected);
void onAuthenticationStatusChanged(bool authenticated);
void onHouseholdIdsReceived(QList<QString> householdIds);
void onFavoritesReceived(QUuid requestId, const QString &householdId, QList<Sonos::FavoriteObject> favorites);
void onPlaylistsReceived(const QString &householdId, QList<Sonos::PlaylistObject> playlists);
void onPlaylistSummaryReceived(const QString &householdId, Sonos::PlaylistSummaryObject playlistSummary);
void onGroupsReceived(const QString &householdId, QList<Sonos::GroupObject> 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

View File

@ -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": [
]
}
]
}
]
}

1623
sonos/sonos.cpp Normal file

File diff suppressed because it is too large Load Diff

299
sonos/sonos.h Normal file
View File

@ -0,0 +1,299 @@
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
* *
* Copyright (C) 2019 Bernhard Trinnes <bernhard.trinnes@nymea.io> *
* *
* 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 *
* <http://www.gnu.org/licenses/>. *
* *
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
#ifndef SONOS_H
#define SONOS_H
#include <QObject>
#include <QTimer>
#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<QByteArray> 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<PlaylistTrackObject> 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<QString> householdIds);
void favoritesReceived(QUuid requestId, const QString &householdId, QList<FavoriteObject> favorites);
void playlistsReceived(const QString &householdId, QList<PlaylistObject> playlists);
void groupsReceived(const QString &householdId, QList<GroupObject> 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

13
sonos/sonos.pro Normal file
View File

@ -0,0 +1,13 @@
include(../plugins.pri)
QT += network
TARGET = $$qtLibraryTarget(nymea_devicepluginsonos)
SOURCES += \
devicepluginsonos.cpp \
sonos.cpp \
HEADERS += \
devicepluginsonos.h \
sonos.h \