/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * Copyright 2013 - 2020, nymea GmbH * Contact: contact@nymea.io * * This file is part of nymea. * This project including source code and documentation is protected by * copyright law, and remains the property of nymea GmbH. All rights, including * reproduction, publication, editing and translation, are reserved. The use of * this project is subject to the terms of a license agreement to be concluded * with nymea GmbH in accordance with the terms of use of nymea GmbH, available * under https://nymea.io/license * * GNU Lesser General Public License Usage * Alternatively, this project may be redistributed and/or modified under the * terms of the GNU Lesser General Public License as published by the Free * Software Foundation; version 3. This project 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 project. If not, see . * * For any further details and any questions please contact us under * contact@nymea.io or see our FAQ/Licensing Information on * https://nymea.io/license/faq * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ #include "integrationpluginsonos.h" #include "integrations/thing.h" #include "network/networkaccessmanager.h" #include "plugininfo.h" #include "types/mediabrowseritem.h" #include #include #include #include IntegrationPluginSonos::IntegrationPluginSonos() { } IntegrationPluginSonos::~IntegrationPluginSonos() { if (m_pluginTimer5sec) hardwareManager()->pluginTimerManager()->unregisterTimer(m_pluginTimer5sec); if (m_pluginTimer60sec) hardwareManager()->pluginTimerManager()->unregisterTimer(m_pluginTimer60sec); } void IntegrationPluginSonos::setupThing(ThingSetupInfo *info) { Thing *thing = info->thing(); if (thing->thingClassId() == sonosConnectionThingClassId) { Sonos *sonos; if (m_setupSonosConnections.keys().contains(thing->id())) { //Fresh thing setup, has already a fresh access token qCDebug(dcSonos()) << "Sonos OAuth setup complete"; sonos = m_setupSonosConnections.take(thing->id()); connect(sonos, &Sonos::connectionChanged, this, &IntegrationPluginSonos::onConnectionChanged); connect(sonos, &Sonos::householdIdsReceived, this, &IntegrationPluginSonos::onHouseholdIdsReceived); connect(sonos, &Sonos::groupsReceived, this, &IntegrationPluginSonos::onGroupsReceived); connect(sonos, &Sonos::playBackStatusReceived, this, &IntegrationPluginSonos::onPlayBackStatusReceived); connect(sonos, &Sonos::metadataStatusReceived, this, &IntegrationPluginSonos::onMetadataStatusReceived); connect(sonos, &Sonos::volumeReceived, this, &IntegrationPluginSonos::onVolumeReceived); connect(sonos, &Sonos::actionExecuted, this, &IntegrationPluginSonos::onActionExecuted); connect(sonos, &Sonos::authenticationStatusChanged, this, &IntegrationPluginSonos::onAuthenticationStatusChanged); connect(sonos, &Sonos::authenticationStatusChanged, info, [info](bool authenticated){ if (authenticated) { info->finish(Thing::ThingErrorNoError); } else { info->finish(Thing::ThingErrorAuthenticationFailure); } }); m_sonosConnections.insert(thing, sonos); return info->finish(Thing::ThingErrorNoError); } else { //thing loaded from the thing database, needs a new access token; pluginStorage()->beginGroup(thing->id().toString()); QByteArray refreshToken = pluginStorage()->value("refresh_token").toByteArray(); pluginStorage()->endGroup(); if (refreshToken.isEmpty()) { info->finish(Thing::ThingErrorAuthenticationFailure); return; } sonos = new Sonos(hardwareManager()->networkManager(), "0a8f6d44-d9d1-4474-bcfa-cfb41f8b66e8", "3095ce48-0c5d-47ce-a1f4-6005c7b8fdb5", this); connect(sonos, &Sonos::connectionChanged, this, &IntegrationPluginSonos::onConnectionChanged); connect(sonos, &Sonos::householdIdsReceived, this, &IntegrationPluginSonos::onHouseholdIdsReceived); connect(sonos, &Sonos::groupsReceived, this, &IntegrationPluginSonos::onGroupsReceived); connect(sonos, &Sonos::playBackStatusReceived, this, &IntegrationPluginSonos::onPlayBackStatusReceived); connect(sonos, &Sonos::metadataStatusReceived, this, &IntegrationPluginSonos::onMetadataStatusReceived); connect(sonos, &Sonos::volumeReceived, this, &IntegrationPluginSonos::onVolumeReceived); connect(sonos, &Sonos::actionExecuted, this, &IntegrationPluginSonos::onActionExecuted); connect(sonos, &Sonos::authenticationStatusChanged, this, &IntegrationPluginSonos::onAuthenticationStatusChanged); connect(sonos, &Sonos::favoritesReceived, this, &IntegrationPluginSonos::onFavoritesReceived); sonos->getAccessTokenFromRefreshToken(refreshToken); m_sonosConnections.insert(thing, sonos); return info->finish(Thing::ThingErrorNoError); } } if (thing->thingClassId() == sonosGroupThingClassId) { return info->finish(Thing::ThingErrorNoError); } qCWarning(dcSonos()) << "Unhandled thing class id in setupDevice" << thing->thingClassId(); } void IntegrationPluginSonos::startPairing(ThingPairingInfo *info) { if (info->thingClassId() == sonosConnectionThingClassId) { 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(Thing::ThingErrorNoError); m_setupSonosConnections.insert(info->thingId(), sonos); return; } qCWarning(dcSonos()) << "Unhandled pairing metod!"; info->finish(Thing::ThingErrorCreationMethodNotSupported); } void IntegrationPluginSonos::confirmPairing(ThingPairingInfo *info, const QString &username, const QString &secret) { Q_UNUSED(username) if (info->thingClassId() == sonosConnectionThingClassId) { 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->thingId()); if (!sonos) { qWarning(dcSonos()) << "No sonos connection found for thing:" << info->thingName(); m_setupSonosConnections.remove(info->thingId()); sonos->deleteLater(); info->finish(Thing::ThingErrorHardwareFailure); return; } sonos->getAccessTokenFromAuthorizationCode(authorizationCode); connect(sonos, &Sonos::authenticationStatusChanged, info, [this, info, sonos](bool authenticated){ if(!authenticated) { qWarning(dcSonos()) << "Authentication process failed" << info->thingName(); m_setupSonosConnections.remove(info->thingId()); sonos->deleteLater(); info->finish(Thing::ThingErrorSetupFailed, 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->thingId().toString()); pluginStorage()->setValue("refresh_token", refreshToken); pluginStorage()->endGroup(); info->finish(Thing::ThingErrorNoError); }); return; } qCWarning(dcSonos()) << "Invalid thingClassId -> no pairing possible with this device"; info->finish(Thing::ThingErrorThingClassNotFound); } void IntegrationPluginSonos::postSetupThing(Thing *thing) { if (!m_pluginTimer5sec) { m_pluginTimer5sec = hardwareManager()->pluginTimerManager()->registerTimer(5); connect(m_pluginTimer5sec, &PluginTimer::timeout, this, [this]() { foreach (Thing *connectionDevice, myThings().filterByThingClassId(sonosConnectionThingClassId)) { Sonos *sonos = m_sonosConnections.value(connectionDevice); if (!sonos) { qWarning(dcSonos()) << "No sonos connection found to" << connectionDevice->name(); continue; } foreach (Thing *groupDevice, myThings().filterByParentId(connectionDevice->id())) { if (groupDevice->thingClassId() == sonosGroupThingClassId) { //get playback status of each group QString groupId = groupDevice->paramValue(sonosGroupThingGroupIdParamTypeId).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 (Thing *thing, myThings().filterByThingClassId(sonosConnectionThingClassId)) { Sonos *sonos = m_sonosConnections.value(thing); if (!sonos) { qWarning(dcSonos()) << "No sonos connection found to" << thing->name(); continue; } //get groups for each household in order to add or remove groups sonos->getHouseholds(); } }); } if (thing->thingClassId() == sonosConnectionThingClassId) { Sonos *sonos = m_sonosConnections.value(thing); sonos->getHouseholds(); } if (thing->thingClassId() == sonosGroupThingClassId) { Thing *parentDevice = myThings().findById(thing->parentId()); Sonos *sonos = m_sonosConnections.value(parentDevice); if (!sonos) { return; } QString groupId = thing->paramValue(sonosGroupThingGroupIdParamTypeId).toString(); sonos->getGroupPlaybackStatus(groupId); sonos->getGroupMetadataStatus(groupId); sonos->getGroupVolume(groupId); } } void IntegrationPluginSonos::startMonitoringAutoThings() { foreach (Thing *thing, myThings()) { if (thing->thingClassId() == sonosGroupThingClassId) { return; } } } void IntegrationPluginSonos::thingRemoved(Thing *thing) { qCDebug(dcSonos) << "Delete " << thing->name(); if (myThings().empty()) { hardwareManager()->pluginTimerManager()->unregisterTimer(m_pluginTimer5sec); hardwareManager()->pluginTimerManager()->unregisterTimer(m_pluginTimer60sec); m_pluginTimer5sec = nullptr; m_pluginTimer60sec = nullptr; } } void IntegrationPluginSonos::executeAction(ThingActionInfo *info) { Thing *thing = info->thing(); Action action = info->action(); if (thing->thingClassId() == sonosGroupThingClassId) { Sonos *sonos = m_sonosConnections.value(myThings().findById(thing->parentId())); QString groupId = thing->paramValue(sonosGroupThingGroupIdParamTypeId).toString(); if (!sonos) { qWarning(dcSonos()) << "Action cannot be executed: Sonos connection not available"; return info->finish(Thing::ThingErrorHardwareNotAvailable, QT_TR_NOOP("Sonos thing 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(Thing::ThingErrorHardwareFailure); } 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(Thing::ThingErrorActionTypeNotFound); } info->finish(Thing::ThingErrorThingClassNotFound); } void IntegrationPluginSonos::browseThing(BrowseResult *result) { Thing *parentDevice = myThings().findById(result->thing()->parentId()); Sonos *sonosConnection = m_sonosConnections.value(parentDevice); if (!sonosConnection) { result->finish(Thing::ThingErrorHardwareNotAvailable); return; } qDebug(dcSonos()) << "Browse Device" << result->itemId(); QString householdId = result->thing()->paramValue(sonosGroupThingHouseholdIdParamTypeId).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(Thing::ThingErrorNoError); } 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(Thing::ThingErrorItemNotFound); } } void IntegrationPluginSonos::browserItem(BrowserItemResult *result) { Thing *parentDevice = myThings().findById(result->thing()->parentId()); Sonos *sonosConnection = m_sonosConnections.value(parentDevice); if (!sonosConnection) { result->finish(Thing::ThingErrorHardwareNotAvailable); return; } qCDebug(dcSonos()) << "Browser Item" << result->itemId(); QString householdId = result->thing()->paramValue(sonosGroupThingHouseholdIdParamTypeId).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(Thing::ThingErrorItemNotFound); } } void IntegrationPluginSonos::executeBrowserItem(BrowserActionInfo *info) { Thing *parentDevice = myThings().findById(info->thing()->parentId()); Sonos *sonosConnection = m_sonosConnections.value(parentDevice); if (!sonosConnection) return; QString groupId = info->thing()->paramValue(sonosGroupThingGroupIdParamTypeId).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(Thing::ThingErrorItemNotFound); } } void IntegrationPluginSonos::onConnectionChanged(bool connected) { Sonos *sonos = static_cast(sender()); Thing *thing = m_sonosConnections.key(sonos); if (!thing) return; thing->setStateValue(sonosConnectionConnectedStateTypeId, connected); foreach (Thing *groupDevice, myThings().filterByParentId(thing->id())) { groupDevice->setStateValue(sonosGroupConnectedStateTypeId, connected); } } void IntegrationPluginSonos::onAuthenticationStatusChanged(bool authenticated) { Sonos *sonosConnection = static_cast(sender()); Thing *thing = m_sonosConnections.key(sonosConnection); if (!thing) return; thing->setStateValue(sonosConnectionLoggedInStateTypeId, authenticated); if (!authenticated) { //refresh access token needs to be refreshed pluginStorage()->beginGroup(thing->id().toString()); QByteArray refreshToken = pluginStorage()->value("refresh_token").toByteArray(); pluginStorage()->endGroup(); sonosConnection->getAccessTokenFromRefreshToken(refreshToken); } } void IntegrationPluginSonos::onHouseholdIdsReceived(QList householdIds) { Sonos *sonos = static_cast(sender()); foreach(QString householdId, householdIds) { sonos->getGroups(householdId); sonos->getPlaylists(householdId); } } void IntegrationPluginSonos::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(Thing::ThingErrorNoError); } 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 IntegrationPluginSonos::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 IntegrationPluginSonos::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 IntegrationPluginSonos::onGroupsReceived(const QString &householdId, QList groupObjects) { Sonos *sonos = static_cast(sender()); Thing *parentDevice = m_sonosConnections.key(sonos); if (!parentDevice) return; QList deviceDescriptors; foreach(Sonos::GroupObject groupObject, groupObjects) { Thing *groupDevice = myThings().findByParams(ParamList() << Param(sonosGroupThingGroupIdParamTypeId, 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 thing, add to the system ThingDescriptor thingDescriptor(sonosGroupThingClassId, groupObject.displayName, "Sonos Group", parentDevice->id()); ParamList params; params.append(Param(sonosGroupThingGroupIdParamTypeId, groupObject.groupId)); params.append(Param(sonosGroupThingHouseholdIdParamTypeId, householdId)); thingDescriptor.setParams(params); deviceDescriptors.append(thingDescriptor); } } if (!deviceDescriptors.isEmpty()) emit autoThingsAppeared(deviceDescriptors); //delete auto devices foreach(Thing *groupDevice, myThings().filterByParentId(parentDevice->id())) { QString groupId = groupDevice->paramValue(sonosGroupThingGroupIdParamTypeId).toString(); bool deviceRemoved = true; foreach (Sonos::GroupObject groupObject, groupObjects) { if(groupObject.groupId == groupId) { deviceRemoved = false; } } if (deviceRemoved) { emit autoThingDisappeared(groupDevice->id()); } } } void IntegrationPluginSonos::onPlayBackStatusReceived(const QString &groupId, Sonos::PlayBackObject playBack) { Thing *thing = myThings().findByParams(ParamList() << Param(sonosGroupThingGroupIdParamTypeId, groupId)); if (!thing) return; thing->setStateValue(sonosGroupShuffleStateTypeId, playBack.playMode.shuffle); if (playBack.playMode.repeatOne) { thing->setStateValue(sonosGroupRepeatStateTypeId, "One"); } else if (playBack.playMode.repeat) { thing->setStateValue(sonosGroupRepeatStateTypeId, "All"); } else { thing->setStateValue(sonosGroupRepeatStateTypeId, "None"); } switch (playBack.playbackState) { case Sonos::PlayBackStateIdle: thing->setStateValue(sonosGroupPlaybackStatusStateTypeId, "Stopped"); break; case Sonos::PlayBackStatePause: thing->setStateValue(sonosGroupPlaybackStatusStateTypeId, "Paused"); break; case Sonos::PlayBackStateBuffering: case Sonos::PlayBackStatePlaying: thing->setStateValue(sonosGroupPlaybackStatusStateTypeId, "Playing"); break; } } void IntegrationPluginSonos::onMetadataStatusReceived(const QString &groupId, Sonos::MetadataStatus metaDataStatus) { Thing *thing = myThings().findByParams(ParamList() << Param(sonosGroupThingGroupIdParamTypeId, groupId)); if (!thing) return; thing->setStateValue(sonosGroupTitleStateTypeId, metaDataStatus.currentItem.track.name); thing->setStateValue(sonosGroupArtistStateTypeId, metaDataStatus.currentItem.track.artist.name); thing->setStateValue(sonosGroupCollectionStateTypeId, metaDataStatus.currentItem.track.album.name); if (!metaDataStatus.currentItem.track.imageUrl.isEmpty()){ thing->setStateValue(sonosGroupArtworkStateTypeId, metaDataStatus.currentItem.track.imageUrl); } else { thing->setStateValue(sonosGroupArtworkStateTypeId, metaDataStatus.container.imageUrl); } } void IntegrationPluginSonos::onVolumeReceived(const QString &groupId, Sonos::VolumeObject groupVolume) { Thing *thing = myThings().findByParams(ParamList() << Param(sonosGroupThingGroupIdParamTypeId, groupId)); if (!thing) return; thing->setStateValue(sonosGroupVolumeStateTypeId, groupVolume.volume); thing->setStateValue(sonosGroupMuteStateTypeId, groupVolume.muted); } void IntegrationPluginSonos::onActionExecuted(QUuid sonosActionId, bool success) { if (m_pendingActions.contains(sonosActionId)) { QPointer info = m_pendingActions.value(sonosActionId); if (info.isNull()) { qCWarning(dcSonos()) << "ThingActionInfo has disappeared. Did it time out?"; return; } if (success) { info->finish(Thing::ThingErrorNoError); } else { info->finish(Thing::ThingErrorHardwareFailure); } } 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(Thing::ThingErrorNoError); } else { info->finish(Thing::ThingErrorHardwareFailure); } } }