From 9b81617c091491a3179401b387500e95df4cd928 Mon Sep 17 00:00:00 2001 From: nymea Date: Tue, 9 Jul 2019 22:05:42 +0200 Subject: [PATCH 01/22] added sonos plug-in --- .gitmodules | 3 + ...a1b6d-c37e-4c9f-a696-1666f9de66e6-en_US.ts | 4 + nymea-plugins.pro | 1 + ...3a12e-36f4-4015-8019-26b659817773-en_US.ts | 4 + sonos/devicepluginsonos.cpp | 279 ++++++++++++++++++ sonos/devicepluginsonos.h | 69 +++++ sonos/devicepluginsonos.json | 176 +++++++++++ sonos/noson | 1 + unipi/.crossbuilder/cache.conf | 2 + 9 files changed, 539 insertions(+) create mode 100644 .gitmodules create mode 100644 modbuscommander/translations/7dda1b6d-c37e-4c9f-a696-1666f9de66e6-en_US.ts create mode 100644 serialportcommander/translations/fe93a12e-36f4-4015-8019-26b659817773-en_US.ts create mode 100644 sonos/devicepluginsonos.cpp create mode 100644 sonos/devicepluginsonos.h create mode 100644 sonos/devicepluginsonos.json create mode 160000 sonos/noson create mode 100644 unipi/.crossbuilder/cache.conf diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 00000000..7581b25b --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "sonos/sonos/noson"] + path = sonos/sonos/noson + url = https://github.com/janbar/noson.git diff --git a/modbuscommander/translations/7dda1b6d-c37e-4c9f-a696-1666f9de66e6-en_US.ts b/modbuscommander/translations/7dda1b6d-c37e-4c9f-a696-1666f9de66e6-en_US.ts new file mode 100644 index 00000000..f7f66d85 --- /dev/null +++ b/modbuscommander/translations/7dda1b6d-c37e-4c9f-a696-1666f9de66e6-en_US.ts @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/nymea-plugins.pro b/nymea-plugins.pro index 9addd3f2..53c48709 100644 --- a/nymea-plugins.pro +++ b/nymea-plugins.pro @@ -40,6 +40,7 @@ PLUGIN_DIRS = \ serialportcommander \ simulation \ snapd \ + sonos \ tasmota \ tcpcommander \ texasinstruments \ diff --git a/serialportcommander/translations/fe93a12e-36f4-4015-8019-26b659817773-en_US.ts b/serialportcommander/translations/fe93a12e-36f4-4015-8019-26b659817773-en_US.ts new file mode 100644 index 00000000..f7f66d85 --- /dev/null +++ b/serialportcommander/translations/fe93a12e-36f4-4015-8019-26b659817773-en_US.ts @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/sonos/devicepluginsonos.cpp b/sonos/devicepluginsonos.cpp new file mode 100644 index 00000000..2499fc21 --- /dev/null +++ b/sonos/devicepluginsonos.cpp @@ -0,0 +1,279 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * * + * Copyright (C) 2019 Bernhard Trinnes . * + * * + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +#include "devicepluginsonos.h" +#include "devices/device.h" +#include "plugininfo.h" + + +#include + +#include +#include + +DevicePluginSonos::DevicePluginSonos() +{ + +} + +DevicePluginSonos::~DevicePluginSonos() +{ + hardwareManager()->pluginTimerManager()->unregisterTimer(m_pluginTimer); +} + +void DevicePluginSonos::init() +{ + qDebug(dcSonos) << "Nymea sonos plug-in using libnoson Copyright (C) 2018 Jean-Luc Barriere"; + m_sonosSystem = new SONOS::System(nullptr, nullptr); +} + +Device::DeviceSetupStatus DevicePluginSonos::setupDevice(Device *device) +{ + if (device->deviceClassId() == sonosDeviceClassId) { + if (!m_sonosSystem->Discover()) { + return Device::DeviceSetupStatusFailure; + } + QString zoneName = device->paramValue(sonosDeviceZoneNameParamTypeId).toString(); + + SONOS::ZoneList zones = m_sonosSystem->GetZoneList(); + + for(SONOS::ZoneList::const_iterator iz = zones.begin(); iz != zones.end(); ++iz) { + + if (iz->second->GetZoneName().c_str() == zoneName) { + if (!m_sonosSystem->ConnectZone(iz->second, nullptr, nullptr)) { + qCDebug(dcSonos()) << "Failed connecting to zone" << zoneName; + return Device::DeviceSetupStatusFailure; + } + } + } + device->setStateValue(sonosConnectedStateTypeId, true); + } + return Device::DeviceSetupStatusSuccess; +} + +void DevicePluginSonos::postSetupDevice(Device *device) +{ + if (device->deviceClassId() == sonosDeviceClassId) { + SONOS::ZonePtr pl = m_sonosSystem->GetConnectedZone(); + uint8_t volume; + uint8_t mute; + for (SONOS::Zone::iterator ip = pl->begin(); ip != pl->end(); ++ip) { + if (!m_sonosSystem->GetPlayer()->GetVolume((*ip)->GetUUID(), &volume)) { + qWarning(dcSonos()) << "Could not get volume for" << (*ip)->GetHost().c_str(); + } else { + device->setStateValue(sonosVolumeStateTypeId, volume); + } + + if (!m_sonosSystem->GetPlayer()->GetMute((*ip)->GetUUID(), &mute)) { + qWarning(dcSonos()) << "Could not get mute state for" << (*ip)->GetHost().c_str(); + } else { + device->setStateValue(sonosMuteStateTypeId, mute); + } + } + + while(m_sonosSystem->GetPlayer()->TransportPropertyEmpty()); + + SONOS::AVTProperty properties = m_sonosSystem->GetPlayer()->GetTransportProperty(); + + qDebug(dcSonos()) << "Transport Status" << properties.TransportStatus.c_str(); + qDebug(dcSonos()) << "Transport State" << properties.TransportState.c_str(); + if (properties.TransportState.c_str() == "PLAYING") { + device->setStateValue(sonosPlaybackStatusStateTypeId, "Playing"); + } else if (properties.TransportState.c_str() == "PAUSED") { + device->setStateValue(sonosPlaybackStatusStateTypeId, "Paused"); + } else if (properties.TransportState.c_str() == "STOPPED") { + device->setStateValue(sonosPlaybackStatusStateTypeId, "Stopped"); + } + + qDebug(dcSonos()) << "AVTransport URI" << properties.AVTransportURI.c_str(); + qDebug(dcSonos()) << "AVTransportTitle" << properties.AVTransportURIMetaData->GetValue("dc:title").c_str(); + qDebug(dcSonos()) << "Current Track" << properties.CurrentTrack; + qDebug(dcSonos()) << "Current Track Duration" << properties.CurrentTrackDuration.c_str(); + qDebug(dcSonos()) << "Current Track URI" << properties.CurrentTrackURI.c_str(); + //device->setStateValue(sonosArtworkStateTypeId, properties.CurrentTrackURI.c_str()); + qDebug(dcSonos()) << "Current Track Title" << properties.CurrentTrackMetaData->GetValue("dc:title").c_str(); + device->setStateValue(sonosTitleStateTypeId, properties.CurrentTrackMetaData->GetValue("dc:title").c_str()); + qDebug(dcSonos()) << "Current Track Album" << properties.CurrentTrackMetaData->GetValue("upnp:album").c_str(); + qDebug(dcSonos()) << "Current Track Artist" << properties.CurrentTrackMetaData->GetValue("dc:creator").c_str(); + device->setStateValue(sonosArtistStateTypeId, properties.CurrentTrackMetaData->GetValue("dc:creator").c_str()); + qDebug(dcSonos()) << "Current Crossfade Mode" << properties.CurrentCrossfadeMode.c_str(); + qDebug(dcSonos()) << "Current Play Mode" << properties.CurrentPlayMode.c_str(); + qDebug(dcSonos()) << "Current TransportActions" << properties.CurrentTransportActions.c_str(); + qDebug(dcSonos()) << "Number Of Tracks" << properties.NumberOfTracks; + qDebug(dcSonos()) << "Alarm Running" << properties.r_AlarmRunning.c_str(); + qDebug(dcSonos()) << "Alarm ID Running" << properties.r_AlarmIDRunning.c_str(); + qDebug(dcSonos()) << "Alarm Logged Start Time" << properties.r_AlarmLoggedStartTime.c_str(); + qDebug(dcSonos()) << "AlarmState" << properties.r_AlarmState.c_str(); + } +} + +void DevicePluginSonos::deviceRemoved(Device *device) +{ + qCDebug(dcSonos) << "Delete " << device->name(); + if (myDevices().empty()) { + delete m_sonosSystem; + } +} + +Device::DeviceError DevicePluginSonos::discoverDevices(const DeviceClassId &deviceClassId, const ParamList ¶ms) +{ + Q_UNUSED(params) + Q_UNUSED(deviceClassId) + + + if (!m_sonosSystem->Discover()) { + return Device::DeviceErrorDeviceNotFound; + } + + QList descriptors; + SONOS::ZoneList zones = m_sonosSystem->GetZoneList(); + + for (SONOS::ZoneList::const_iterator it = zones.begin(); it != zones.end(); ++it) { + qDebug(dcSonos()) << "Found zone" << it->second->GetZoneName().c_str(); + DeviceDescriptor descriptor(sonosDeviceClassId, it->second->GetZoneName().c_str()); + ParamList params; + params << Param(sonosDeviceZoneNameParamTypeId, it->second->GetZoneName().c_str()); + descriptor.setParams(params); + descriptors << descriptor; + } + emit devicesDiscovered(sonosDeviceClassId, descriptors); + return Device::DeviceErrorAsync; +} + +Device::DeviceError DevicePluginSonos::executeAction(Device *device, const Action &action) +{ + Q_UNUSED(action) + if (device->deviceClassId() == sonosDeviceClassId) { + + if (action.actionTypeId() == sonosPlayActionTypeId) { + if (!m_sonosSystem->GetPlayer()->Play()) { + return Device::DeviceErrorHardwareFailure; + } + return Device::DeviceErrorNoError; + } + + if (action.actionTypeId() == sonosShuffleActionTypeId) { + SONOS::PlayMode_t mode = SONOS::PlayMode_t::PlayMode_NORMAL;; + if (action.param(sonosShuffleActionShuffleParamTypeId).value().toBool()) { + mode = SONOS::PlayMode_t::PlayMode_NORMAL; + } + if (!m_sonosSystem->GetPlayer()->SetPlayMode(mode)) { + return Device::DeviceErrorHardwareFailure; + } + return Device::DeviceErrorNoError; + } + + if (action.actionTypeId() == sonosRepeatActionTypeId) { + SONOS::PlayMode_t mode; + if (action.param(sonosRepeatActionRepeatParamTypeId).value().toString() == "None") { + mode = SONOS::PlayMode_t::PlayMode_NORMAL; + } else if (action.param(sonosShuffleActionShuffleParamTypeId).value().toString() == "One") { + mode = SONOS::PlayMode_t::PlayMode_REPEAT_ONE; + } else if (action.param(sonosShuffleActionShuffleParamTypeId).value().toString() == "All") { + mode = SONOS::PlayMode_t::PlayMode_REPEAT_ALL; + } else { + return Device::DeviceErrorHardwareFailure; + } + + if (!m_sonosSystem->GetPlayer()->SetPlayMode(mode)) { + return Device::DeviceErrorHardwareFailure; + } + return Device::DeviceErrorNoError; + } + + if (action.actionTypeId() == sonosPauseActionTypeId) { + if (!m_sonosSystem->GetPlayer()->Pause()) { + return Device::DeviceErrorHardwareFailure; + } + return Device::DeviceErrorNoError; + } + + if (action.actionTypeId() == sonosStopActionTypeId) { + if (!m_sonosSystem->GetPlayer()->Stop()) { + return Device::DeviceErrorHardwareFailure; + } + return Device::DeviceErrorNoError; + } + + if (action.actionTypeId() == sonosMuteActionTypeId) { + bool mute = action.param(sonosMuteActionMuteParamTypeId).value().toBool(); + + SONOS::ZonePtr pl = m_sonosSystem->GetConnectedZone(); + for (SONOS::Zone::iterator ip = pl->begin(); ip != pl->end(); ++ip) { + if (!m_sonosSystem->GetPlayer()->SetMute((*ip)->GetUUID(), mute)) { + qWarning(dcSonos()) << "Could not set mute state for" << (*ip)->GetHost().c_str(); + return Device::DeviceErrorHardwareFailure; + } + } + return Device::DeviceErrorNoError; + } + + if (action.actionTypeId() == sonosSkipNextActionTypeId) { + if (!m_sonosSystem->GetPlayer()->Next()) { + return Device::DeviceErrorHardwareFailure; + } + return Device::DeviceErrorNoError; + } + + if (action.actionTypeId() == sonosSkipBackActionTypeId) { + if(!m_sonosSystem->GetPlayer()->Previous()) { + return Device::DeviceErrorHardwareFailure; + } + return Device::DeviceErrorNoError; + } + + if (action.actionTypeId() == sonosSkipBackActionTypeId) { + int volume = action.param(sonosVolumeActionVolumeParamTypeId).value().toInt(); + + SONOS::ZonePtr pl = m_sonosSystem->GetConnectedZone(); + for (SONOS::Zone::iterator ip = pl->begin(); ip != pl->end(); ++ip) { + if (!m_sonosSystem->GetPlayer()->SetVolume((*ip)->GetUUID(), volume)) { + qWarning(dcSonos()) << "Could not set volume for" << (*ip)->GetHost().c_str(); + return Device::DeviceErrorHardwareFailure; + } + } + return Device::DeviceErrorNoError; + } + return Device::DeviceErrorActionTypeNotFound; + } + return Device::DeviceErrorDeviceClassNotFound; +} + +void DevicePluginSonos::onPluginTimer() +{ +} + +void DevicePluginSonos::handleEventCB(void* handle) +{ + Q_UNUSED(handle); + /*unsigned char mask = m_sonosSystem->LastEvents(); + if ((mask & SONOS::SVCEvent_TransportChanged)) + qDebug(dcSonos()) << "Event Transport changed"; + if ((mask & SONOS::SVCEvent_AlarmClockChanged)) + qDebug(dcSonos()) << "Alarm clock changed"; + if ((mask & SONOS::SVCEvent_ZGTopologyChanged)) + qDebug(dcSonos()) << "ZG Topology changed"; + if ((mask & SONOS::SVCEvent_ContentDirectoryChanged)) + qDebug(dcSonos()) << "Content directory changed"; + if ((mask & SONOS::SVCEvent_RenderingControlChanged)) + qDebug(dcSonos()) << "Rendering control changed";*/ +} diff --git a/sonos/devicepluginsonos.h b/sonos/devicepluginsonos.h new file mode 100644 index 00000000..169cd1eb --- /dev/null +++ b/sonos/devicepluginsonos.h @@ -0,0 +1,69 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * * + * Copyright (C) 2019 Bernhard Trinnes . * + * * + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +#ifndef DEVICEPLUGINSONOS_H +#define DEVICEPLUGINSONOS_H + +#include "devices/deviceplugin.h" +#include "plugintimer.h" + +#include + +#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 init() override; + Device::DeviceSetupStatus setupDevice(Device *device) override; + void postSetupDevice(Device *device) override; + void deviceRemoved(Device *device) override; + Device::DeviceError discoverDevices(const DeviceClassId &deviceClassId, const ParamList ¶ms) override; + Device::DeviceError executeAction(Device *device, const Action &action) override; + +private: + SONOS::System *m_sonosSystem = nullptr; + PluginTimer *m_pluginTimer; + + static void handleEventCB(void *handle); + +private slots: + void onPluginTimer(); + /*void onConnectionChanged(); + void onStateChanged(); + void onActionExecuted(int actionId, bool success); + void versionDataReceived(const QVariantMap &data); + void onSetupFinished(const QVariantMap &data); + + void onPlaybackStatusChanged(const QString &playbackStatus);*/ +}; + +#endif // DEVICEPLUGINSONOS_H diff --git a/sonos/devicepluginsonos.json b/sonos/devicepluginsonos.json new file mode 100644 index 00000000..4e239fb7 --- /dev/null +++ b/sonos/devicepluginsonos.json @@ -0,0 +1,176 @@ +{ + "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": "sonos", + "displayName": "Sonos", + "interfaces": ["extendedvolumecontroller", "mediametadataprovider", "shufflerepeat", "connectable"], + "createMethods": ["discovery"], + "paramTypes": [ + { + "id": "defc44cd-2ffb-4af1-b348-d6a3474c7515", + "name": "zoneName", + "displayName": "Zone name", + "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": "player play" + }, + { + "id": "99498b1c-e9c0-480a-9e91-662ee79ba976", + "name": "onPlayerPause", + "displayName": "player pause" + }, + { + "id": "a02ce255-3abb-435d-a92e-7f99c952ecb2", + "name": "onPlayerStop", + "displayName": "player 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": "skipNext" + } + ] + } + ] + } + ] +} + diff --git a/sonos/noson b/sonos/noson new file mode 160000 index 00000000..569f2b30 --- /dev/null +++ b/sonos/noson @@ -0,0 +1 @@ +Subproject commit 569f2b3087d2ba39bfe796bd6dd20b767fb68eb7 diff --git a/unipi/.crossbuilder/cache.conf b/unipi/.crossbuilder/cache.conf new file mode 100644 index 00000000..5aa1fd54 --- /dev/null +++ b/unipi/.crossbuilder/cache.conf @@ -0,0 +1,2 @@ +TARGET_ARCH=armhf +TARGET_UBUNTU=stretch From 805595588f83fad7ad383aadd8b0e1dc229f2a38 Mon Sep 17 00:00:00 2001 From: nymea Date: Tue, 9 Jul 2019 22:20:52 +0200 Subject: [PATCH 02/22] removed unnecessary files --- .../7dda1b6d-c37e-4c9f-a696-1666f9de66e6-en_US.ts | 4 ---- .../fe93a12e-36f4-4015-8019-26b659817773-en_US.ts | 4 ---- 2 files changed, 8 deletions(-) delete mode 100644 modbuscommander/translations/7dda1b6d-c37e-4c9f-a696-1666f9de66e6-en_US.ts delete mode 100644 serialportcommander/translations/fe93a12e-36f4-4015-8019-26b659817773-en_US.ts diff --git a/modbuscommander/translations/7dda1b6d-c37e-4c9f-a696-1666f9de66e6-en_US.ts b/modbuscommander/translations/7dda1b6d-c37e-4c9f-a696-1666f9de66e6-en_US.ts deleted file mode 100644 index f7f66d85..00000000 --- a/modbuscommander/translations/7dda1b6d-c37e-4c9f-a696-1666f9de66e6-en_US.ts +++ /dev/null @@ -1,4 +0,0 @@ - - - - \ No newline at end of file diff --git a/serialportcommander/translations/fe93a12e-36f4-4015-8019-26b659817773-en_US.ts b/serialportcommander/translations/fe93a12e-36f4-4015-8019-26b659817773-en_US.ts deleted file mode 100644 index f7f66d85..00000000 --- a/serialportcommander/translations/fe93a12e-36f4-4015-8019-26b659817773-en_US.ts +++ /dev/null @@ -1,4 +0,0 @@ - - - - \ No newline at end of file From 20022c7d8dca3398d92d696a9fceded9e979bfc5 Mon Sep 17 00:00:00 2001 From: nymea Date: Wed, 10 Jul 2019 08:06:19 +0200 Subject: [PATCH 03/22] removed unwanted files --- sonos/devicepluginsonos.cpp | 1 + sonos/devicepluginsonos.h | 3 ++- unipi/.crossbuilder/cache.conf | 2 -- 3 files changed, 3 insertions(+), 3 deletions(-) delete mode 100644 unipi/.crossbuilder/cache.conf diff --git a/sonos/devicepluginsonos.cpp b/sonos/devicepluginsonos.cpp index 2499fc21..3d53a4bc 100644 --- a/sonos/devicepluginsonos.cpp +++ b/sonos/devicepluginsonos.cpp @@ -89,6 +89,7 @@ void DevicePluginSonos::postSetupDevice(Device *device) device->setStateValue(sonosMuteStateTypeId, mute); } } + m_sonosSystem->GetPlayer()->MakeFilePictureUrl(); while(m_sonosSystem->GetPlayer()->TransportPropertyEmpty()); diff --git a/sonos/devicepluginsonos.h b/sonos/devicepluginsonos.h index 169cd1eb..1de68f50 100644 --- a/sonos/devicepluginsonos.h +++ b/sonos/devicepluginsonos.h @@ -52,8 +52,9 @@ public: private: SONOS::System *m_sonosSystem = nullptr; PluginTimer *m_pluginTimer; + void updateZoneInfo(); - static void handleEventCB(void *handle); + void handleEventCB(void *handle); private slots: void onPluginTimer(); diff --git a/unipi/.crossbuilder/cache.conf b/unipi/.crossbuilder/cache.conf deleted file mode 100644 index 5aa1fd54..00000000 --- a/unipi/.crossbuilder/cache.conf +++ /dev/null @@ -1,2 +0,0 @@ -TARGET_ARCH=armhf -TARGET_UBUNTU=stretch From afa6dcacdca8860b40d8fe7df6d96c1158ab1fe7 Mon Sep 17 00:00:00 2001 From: nymea Date: Thu, 11 Jul 2019 13:44:27 +0200 Subject: [PATCH 04/22] add missing .pro file --- sonos/devicepluginsonos.cpp | 7 +++---- sonos/sonos.pro | 16 ++++++++++++++++ 2 files changed, 19 insertions(+), 4 deletions(-) create mode 100644 sonos/sonos.pro diff --git a/sonos/devicepluginsonos.cpp b/sonos/devicepluginsonos.cpp index 3d53a4bc..6e8ee10a 100644 --- a/sonos/devicepluginsonos.cpp +++ b/sonos/devicepluginsonos.cpp @@ -89,7 +89,6 @@ void DevicePluginSonos::postSetupDevice(Device *device) device->setStateValue(sonosMuteStateTypeId, mute); } } - m_sonosSystem->GetPlayer()->MakeFilePictureUrl(); while(m_sonosSystem->GetPlayer()->TransportPropertyEmpty()); @@ -97,11 +96,11 @@ void DevicePluginSonos::postSetupDevice(Device *device) qDebug(dcSonos()) << "Transport Status" << properties.TransportStatus.c_str(); qDebug(dcSonos()) << "Transport State" << properties.TransportState.c_str(); - if (properties.TransportState.c_str() == "PLAYING") { + if (QString(properties.TransportState.c_str()) == "PLAYING") { device->setStateValue(sonosPlaybackStatusStateTypeId, "Playing"); - } else if (properties.TransportState.c_str() == "PAUSED") { + } else if (QString(properties.TransportState.c_str()) == "PAUSED") { device->setStateValue(sonosPlaybackStatusStateTypeId, "Paused"); - } else if (properties.TransportState.c_str() == "STOPPED") { + } else if (QString(properties.TransportState.c_str()) == "STOPPED") { device->setStateValue(sonosPlaybackStatusStateTypeId, "Stopped"); } diff --git a/sonos/sonos.pro b/sonos/sonos.pro new file mode 100644 index 00000000..33b7f741 --- /dev/null +++ b/sonos/sonos.pro @@ -0,0 +1,16 @@ +include(../plugins.pri) + +QT += network + +LIBS += -lnoson + +TARGET = $$qtLibraryTarget(nymea_devicepluginsonos) + +SOURCES += \ + devicepluginsonos.cpp \ + +HEADERS += \ + devicepluginsonos.h \ + + + From 2e827c8905db54f41a7cdca7b9e3b8f3b3d5da26 Mon Sep 17 00:00:00 2001 From: Bernhard Trinnes Date: Mon, 12 Aug 2019 19:13:12 +0200 Subject: [PATCH 05/22] changed to Sonos command API --- sonos/devicepluginsonos.cpp | 13 +++++-------- sonos/devicepluginsonos.h | 17 +++-------------- sonos/devicepluginsonos.json | 18 +++++++++++++++--- sonos/noson | 1 - sonos/sonos.pro | 5 ++--- 5 files changed, 25 insertions(+), 29 deletions(-) delete mode 160000 sonos/noson diff --git a/sonos/devicepluginsonos.cpp b/sonos/devicepluginsonos.cpp index 6e8ee10a..c9562d33 100644 --- a/sonos/devicepluginsonos.cpp +++ b/sonos/devicepluginsonos.cpp @@ -24,9 +24,6 @@ #include "devices/device.h" #include "plugininfo.h" - -#include - #include #include @@ -35,19 +32,19 @@ DevicePluginSonos::DevicePluginSonos() } + DevicePluginSonos::~DevicePluginSonos() { hardwareManager()->pluginTimerManager()->unregisterTimer(m_pluginTimer); } -void DevicePluginSonos::init() -{ - qDebug(dcSonos) << "Nymea sonos plug-in using libnoson Copyright (C) 2018 Jean-Luc Barriere"; - m_sonosSystem = new SONOS::System(nullptr, nullptr); -} Device::DeviceSetupStatus DevicePluginSonos::setupDevice(Device *device) { + if (!sonos) { + + } + if (device->deviceClassId() == sonosDeviceClassId) { if (!m_sonosSystem->Discover()) { return Device::DeviceSetupStatusFailure; diff --git a/sonos/devicepluginsonos.h b/sonos/devicepluginsonos.h index 1de68f50..2b4e9377 100644 --- a/sonos/devicepluginsonos.h +++ b/sonos/devicepluginsonos.h @@ -25,8 +25,7 @@ #include "devices/deviceplugin.h" #include "plugintimer.h" - -#include +#include "sonos.h" #include #include @@ -42,7 +41,6 @@ public: explicit DevicePluginSonos(); ~DevicePluginSonos() override; - void init() override; Device::DeviceSetupStatus setupDevice(Device *device) override; void postSetupDevice(Device *device) override; void deviceRemoved(Device *device) override; @@ -50,21 +48,12 @@ public: Device::DeviceError executeAction(Device *device, const Action &action) override; private: - SONOS::System *m_sonosSystem = nullptr; - PluginTimer *m_pluginTimer; - void updateZoneInfo(); - - void handleEventCB(void *handle); + Sonos *sonos = nullptr; private slots: void onPluginTimer(); - /*void onConnectionChanged(); - void onStateChanged(); - void onActionExecuted(int actionId, bool success); - void versionDataReceived(const QVariantMap &data); - void onSetupFinished(const QVariantMap &data); + void onConnectionChanged(); - void onPlaybackStatusChanged(const QString &playbackStatus);*/ }; #endif // DEVICEPLUGINSONOS_H diff --git a/sonos/devicepluginsonos.json b/sonos/devicepluginsonos.json index 4e239fb7..7992e42a 100644 --- a/sonos/devicepluginsonos.json +++ b/sonos/devicepluginsonos.json @@ -4,14 +4,26 @@ "displayName": "Sonos", "vendors": [ { + "id": "30a60752-d06f-4ec9-a4e1-9810a5d22fa3", "name": "sonos", - "displayName": "Sonos", + "displayName": "Sono", "deviceClasses": [ { "id": "22df416d-7732-44f1-b6b9-e41296211178", - "name": "sonos", - "displayName": "Sonos", + "name": "sonosGroup", + "displayName": "Sonos group", + "interfaces": ["gateway", "connectable"], + "createMethods": ["discovery"], + "paramTypes": [ + { + } + ] + }, + { + "id": "22df416d-7732-44f1-b6b9-e41296211178", + "name": "sonosGroup", + "displayName": "Sonos group", "interfaces": ["extendedvolumecontroller", "mediametadataprovider", "shufflerepeat", "connectable"], "createMethods": ["discovery"], "paramTypes": [ diff --git a/sonos/noson b/sonos/noson deleted file mode 160000 index 569f2b30..00000000 --- a/sonos/noson +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 569f2b3087d2ba39bfe796bd6dd20b767fb68eb7 diff --git a/sonos/sonos.pro b/sonos/sonos.pro index 33b7f741..a107f02e 100644 --- a/sonos/sonos.pro +++ b/sonos/sonos.pro @@ -2,15 +2,14 @@ include(../plugins.pri) QT += network -LIBS += -lnoson - TARGET = $$qtLibraryTarget(nymea_devicepluginsonos) SOURCES += \ devicepluginsonos.cpp \ + sonos.cpp \ HEADERS += \ devicepluginsonos.h \ - + sonos.h \ From d4c0bd41c84ad9905fbc69bf24f3f995755bb4dd Mon Sep 17 00:00:00 2001 From: Bernhard Trinnes Date: Mon, 12 Aug 2019 19:13:59 +0200 Subject: [PATCH 06/22] changed to Sonos command API --- sonos/sonos.cpp | 72 ++++++++++++++++ sonos/sonos.h | 219 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 291 insertions(+) create mode 100644 sonos/sonos.cpp create mode 100644 sonos/sonos.h diff --git a/sonos/sonos.cpp b/sonos/sonos.cpp new file mode 100644 index 00000000..919c9c47 --- /dev/null +++ b/sonos/sonos.cpp @@ -0,0 +1,72 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * * + * 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 + +Sonos::Sonos(QObject *parent) +{ +} + +void Sonos::authenticate(const QString &username, const QString &password) +{ + //get oauth autherisation + + //get accesst token +} + +void Sonos::getHouseholds() +{ + QNetworkRequest request; + request.setHeader(QNetworkRequest::KnownHeaders::ContentTypeHeader, "application/json"); + request.setRawHeader("Authorization", "Bearer" + m_bearerToken); + request.setUrl(m_baseControlUrl + "/households"); + QNetworkReply *reply = QNetworkAccessManager.get(request); + connect(reply, &QNetworkReply::finished, this [this] { + int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); + + // Check HTTP status code + if (status != 200 || reply->error() != QNetworkReply::NoError) { + qCWarning(dcSonos()) << "Request error:" << status << reply->errorString(); + return; + } + QJsonDocument data = reply->readAll(); + if (!data.isObject()) + return; + + + QList households; + emit householdObjectsReceived(households); + }); +} + +void Sonos::getPlayerVolume(int playerId) +{ + QNetworkRequest request; + + QJsonObject object; + object. +} + + diff --git a/sonos/sonos.h b/sonos/sonos.h new file mode 100644 index 00000000..832ab065 --- /dev/null +++ b/sonos/sonos.h @@ -0,0 +1,219 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * * + * 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 "devices/device.h" + +class Sonos : public QObject +{ + Q_OBJECT +public: + + enum PlayMode { + Repeat, + RepeatOne, + Shuffle, + Crossfade + }; + + /* + * Represents a Sonos household.*/ + struct HouseholdObject { + QString id; //Identifies a Sonos household. + QString name; //A user-displayable name of the Sonos household + }; + + /* + * 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 FavouriteObject { + QString id; + QString name; + QString description; + QString imageUrl; + }; + + struct PlaylistObject + { + QString id; + QString name; + QString type; + QString trackCount; + }; + + struct PlayerSettingsObject + { + int volumeMode; + float volumeScalingFactor; + bool monoMode; + bool wifiDisabled; + }; + /* + * 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 + { + bool canCrossfade; + bool canSkip; + int durationMillis; + }; + + /* 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; + }; + + /* 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; + }; + + /* The artist of the track. */ + struct ArtistObject + { + QString name; + QString imageUrl; + MusicObjectId id; + // tags enum + }; + + struct AlbumObject + { + QString name; + ArtistObject artist; + QString + + }; + + struct ContainerObject + { + QString name; + QString type; + MusicObjectId id; + ServiceObject service; + QString imageUrl; + //tags enum + }; + + explicit Sonos(QByteArray apiKey, QObject *parent = nullptr); + void authenticate(const QString &username, const QString &password); + + void getHouseholds(); + + void cancelAudioClip(); + void loadAudioClip(); + + void getFavorites(); + void loadFavorite(); + + void getGroups(); + void createGroup(); + void modifyGroupMembers(); + void setGroupMembers(); + + //group volume + void getGroupVolume(int groupId); //Get the volume and mute state of a group. + void setGroupVolume(int groupId, int volume); //Set group volume to a specific level and unmute the group if muted. + void setGroupMute(int groupId, bool mute); //Mute and unmute the group. + void setGroupRelativeVolume(int groupId, int volumeDelta); //Increase or decrease group volume. + + //playback + void getPlaybackStatus(); + void loadLineIn(); + void play(); + void pause(); + void seek(); + void seekRelative(); + void setPlayModes(); + void skipToNextTrack(); + void skipToPreviousTrack(); + void togglePlayPause(); + + //playbackMetadata + + // playerVolume + void getPlayerVolume(int playerId); + void setPlayerVolume(int playerId, int volume); + void setPlayerRelativeVolume(int playerId, int volumeDelta); + void setPlayerMute(int playerId, bool mute); + + //Playlists API namespace + void getPlaylist(); + void getPlaylists(); + void loadPlaylist(); + + //Settings + void getPlayerSettings(); + void setPlayerSettings(); + +private: + QUrl m_baseAuthorizationUrl = "api.sonos.com/login/v3/oauth"; + QUrl m_baseControlUrl = "api.ws.sonos.com/control/api/v1"; + +private slots: + + +signals: + void authenticationSuccessfull(); + void authenticationFailed(const QString &reason); + + +public slots: + +}; + +#endif // SONOS_H From 85060d152c5330c8886902c0d31e6a5ba347557d Mon Sep 17 00:00:00 2001 From: nymea Date: Tue, 13 Aug 2019 09:45:22 +0200 Subject: [PATCH 07/22] added oauth --- sonos/devicepluginsonos.cpp | 192 +++++++++--------------------- sonos/devicepluginsonos.h | 4 +- sonos/devicepluginsonos.json | 21 ++-- sonos/oauth.cpp | 221 +++++++++++++++++++++++++++++++++++ sonos/oauth.h | 71 +++++++++++ sonos/sonos.cpp | 26 ++--- sonos/sonos.h | 15 ++- sonos/sonos.pro | 2 + 8 files changed, 388 insertions(+), 164 deletions(-) create mode 100644 sonos/oauth.cpp create mode 100644 sonos/oauth.h diff --git a/sonos/devicepluginsonos.cpp b/sonos/devicepluginsonos.cpp index c9562d33..24f61306 100644 --- a/sonos/devicepluginsonos.cpp +++ b/sonos/devicepluginsonos.cpp @@ -41,85 +41,44 @@ DevicePluginSonos::~DevicePluginSonos() Device::DeviceSetupStatus DevicePluginSonos::setupDevice(Device *device) { - if (!sonos) { + if (!m_pluginTimer) { + + } + if (device->deviceClassId() == sonosConnectionDeviceClassId) { + + Sonos *sonos = new Sonos("0a8f6d44-d9d1-4474-bcfa-cfb41f8b66e8", this); + pluginStorage()->beginGroup(device->id().toString()); + QString username = pluginStorage()->value("username").toString(); + QString password = pluginStorage()->value("password").toString(); + pluginStorage()->endGroup(); + + sonos->authenticate(username, password); + + m_sonosConnections.insert(device->id(), sonos); + connect(sonos, &Sonos::authenticationFinished, this, [this, sonos](bool success){ + if (success) { + } else { + qCWarning(dcSonos()) << "Cannot authenticate to Sonos api"; + } + }); } - if (device->deviceClassId() == sonosDeviceClassId) { - if (!m_sonosSystem->Discover()) { - return Device::DeviceSetupStatusFailure; - } - QString zoneName = device->paramValue(sonosDeviceZoneNameParamTypeId).toString(); + if (device->deviceClassId() == sonosGroupDeviceClassId) { - SONOS::ZoneList zones = m_sonosSystem->GetZoneList(); - - for(SONOS::ZoneList::const_iterator iz = zones.begin(); iz != zones.end(); ++iz) { - - if (iz->second->GetZoneName().c_str() == zoneName) { - if (!m_sonosSystem->ConnectZone(iz->second, nullptr, nullptr)) { - qCDebug(dcSonos()) << "Failed connecting to zone" << zoneName; - return Device::DeviceSetupStatusFailure; - } - } - } - device->setStateValue(sonosConnectedStateTypeId, true); + //set parent ID } return Device::DeviceSetupStatusSuccess; } void DevicePluginSonos::postSetupDevice(Device *device) { - if (device->deviceClassId() == sonosDeviceClassId) { - SONOS::ZonePtr pl = m_sonosSystem->GetConnectedZone(); - uint8_t volume; - uint8_t mute; - for (SONOS::Zone::iterator ip = pl->begin(); ip != pl->end(); ++ip) { - if (!m_sonosSystem->GetPlayer()->GetVolume((*ip)->GetUUID(), &volume)) { - qWarning(dcSonos()) << "Could not get volume for" << (*ip)->GetHost().c_str(); - } else { - device->setStateValue(sonosVolumeStateTypeId, volume); - } + if (device->deviceClassId() == sonosConnectionDeviceClassId) { - if (!m_sonosSystem->GetPlayer()->GetMute((*ip)->GetUUID(), &mute)) { - qWarning(dcSonos()) << "Could not get mute state for" << (*ip)->GetHost().c_str(); - } else { - device->setStateValue(sonosMuteStateTypeId, mute); - } - } + } - while(m_sonosSystem->GetPlayer()->TransportPropertyEmpty()); + if (device->deviceClassId() == sonosGroupDeviceClassId) { - SONOS::AVTProperty properties = m_sonosSystem->GetPlayer()->GetTransportProperty(); - - qDebug(dcSonos()) << "Transport Status" << properties.TransportStatus.c_str(); - qDebug(dcSonos()) << "Transport State" << properties.TransportState.c_str(); - if (QString(properties.TransportState.c_str()) == "PLAYING") { - device->setStateValue(sonosPlaybackStatusStateTypeId, "Playing"); - } else if (QString(properties.TransportState.c_str()) == "PAUSED") { - device->setStateValue(sonosPlaybackStatusStateTypeId, "Paused"); - } else if (QString(properties.TransportState.c_str()) == "STOPPED") { - device->setStateValue(sonosPlaybackStatusStateTypeId, "Stopped"); - } - - qDebug(dcSonos()) << "AVTransport URI" << properties.AVTransportURI.c_str(); - qDebug(dcSonos()) << "AVTransportTitle" << properties.AVTransportURIMetaData->GetValue("dc:title").c_str(); - qDebug(dcSonos()) << "Current Track" << properties.CurrentTrack; - qDebug(dcSonos()) << "Current Track Duration" << properties.CurrentTrackDuration.c_str(); - qDebug(dcSonos()) << "Current Track URI" << properties.CurrentTrackURI.c_str(); - //device->setStateValue(sonosArtworkStateTypeId, properties.CurrentTrackURI.c_str()); - qDebug(dcSonos()) << "Current Track Title" << properties.CurrentTrackMetaData->GetValue("dc:title").c_str(); - device->setStateValue(sonosTitleStateTypeId, properties.CurrentTrackMetaData->GetValue("dc:title").c_str()); - qDebug(dcSonos()) << "Current Track Album" << properties.CurrentTrackMetaData->GetValue("upnp:album").c_str(); - qDebug(dcSonos()) << "Current Track Artist" << properties.CurrentTrackMetaData->GetValue("dc:creator").c_str(); - device->setStateValue(sonosArtistStateTypeId, properties.CurrentTrackMetaData->GetValue("dc:creator").c_str()); - qDebug(dcSonos()) << "Current Crossfade Mode" << properties.CurrentCrossfadeMode.c_str(); - qDebug(dcSonos()) << "Current Play Mode" << properties.CurrentPlayMode.c_str(); - qDebug(dcSonos()) << "Current TransportActions" << properties.CurrentTransportActions.c_str(); - qDebug(dcSonos()) << "Number Of Tracks" << properties.NumberOfTracks; - qDebug(dcSonos()) << "Alarm Running" << properties.r_AlarmRunning.c_str(); - qDebug(dcSonos()) << "Alarm ID Running" << properties.r_AlarmIDRunning.c_str(); - qDebug(dcSonos()) << "Alarm Logged Start Time" << properties.r_AlarmLoggedStartTime.c_str(); - qDebug(dcSonos()) << "AlarmState" << properties.r_AlarmState.c_str(); } } @@ -127,100 +86,59 @@ void DevicePluginSonos::deviceRemoved(Device *device) { qCDebug(dcSonos) << "Delete " << device->name(); if (myDevices().empty()) { - delete m_sonosSystem; } } -Device::DeviceError DevicePluginSonos::discoverDevices(const DeviceClassId &deviceClassId, const ParamList ¶ms) -{ - Q_UNUSED(params) - Q_UNUSED(deviceClassId) - - - if (!m_sonosSystem->Discover()) { - return Device::DeviceErrorDeviceNotFound; - } - - QList descriptors; - SONOS::ZoneList zones = m_sonosSystem->GetZoneList(); - - for (SONOS::ZoneList::const_iterator it = zones.begin(); it != zones.end(); ++it) { - qDebug(dcSonos()) << "Found zone" << it->second->GetZoneName().c_str(); - DeviceDescriptor descriptor(sonosDeviceClassId, it->second->GetZoneName().c_str()); - ParamList params; - params << Param(sonosDeviceZoneNameParamTypeId, it->second->GetZoneName().c_str()); - descriptor.setParams(params); - descriptors << descriptor; - } - emit devicesDiscovered(sonosDeviceClassId, descriptors); - return Device::DeviceErrorAsync; -} Device::DeviceError DevicePluginSonos::executeAction(Device *device, const Action &action) { Q_UNUSED(action) - if (device->deviceClassId() == sonosDeviceClassId) { + if (device->deviceClassId() == sonosGroupDeviceClassId) { + Sonos *sonos = m_sonosConnections.value(device->parentId()); + int groupId = device->paramValue(sonosGroupDe) + if (!sonos) + return Device::DeviceErrorInvalidParameter; - if (action.actionTypeId() == sonosPlayActionTypeId) { - if (!m_sonosSystem->GetPlayer()->Play()) { - return Device::DeviceErrorHardwareFailure; - } - return Device::DeviceErrorNoError; + if (action.actionTypeId() == sonosGroupPlayActionTypeId) { + sonos->play(); + return Device::DeviceErrorAsync; } - if (action.actionTypeId() == sonosShuffleActionTypeId) { - SONOS::PlayMode_t mode = SONOS::PlayMode_t::PlayMode_NORMAL;; - if (action.param(sonosShuffleActionShuffleParamTypeId).value().toBool()) { - mode = SONOS::PlayMode_t::PlayMode_NORMAL; - } - if (!m_sonosSystem->GetPlayer()->SetPlayMode(mode)) { - return Device::DeviceErrorHardwareFailure; - } - return Device::DeviceErrorNoError; + if (action.actionTypeId() == sonosGroupShuffleActionTypeId) { + + return Device::DeviceErrorAsync; } - if (action.actionTypeId() == sonosRepeatActionTypeId) { - SONOS::PlayMode_t mode; - if (action.param(sonosRepeatActionRepeatParamTypeId).value().toString() == "None") { - mode = SONOS::PlayMode_t::PlayMode_NORMAL; - } else if (action.param(sonosShuffleActionShuffleParamTypeId).value().toString() == "One") { - mode = SONOS::PlayMode_t::PlayMode_REPEAT_ONE; - } else if (action.param(sonosShuffleActionShuffleParamTypeId).value().toString() == "All") { - mode = SONOS::PlayMode_t::PlayMode_REPEAT_ALL; + if (action.actionTypeId() == sonosGroupRepeatActionTypeId) { + + if (action.param(sonosGroupRepeatActionRepeatParamTypeId).value().toString() == "None") { + + } else if (action.param(sonosGroupShuffleActionShuffleParamTypeId).value().toString() == "One") { + + } else if (action.param(sonosGroupShuffleActionShuffleParamTypeId).value().toString() == "All") { + } else { return Device::DeviceErrorHardwareFailure; } - if (!m_sonosSystem->GetPlayer()->SetPlayMode(mode)) { - return Device::DeviceErrorHardwareFailure; - } + + return Device::DeviceErrorAsync; + } + + if (action.actionTypeId() == sonosGroupPauseActionTypeId) { + sonos->pause(); return Device::DeviceErrorNoError; } - if (action.actionTypeId() == sonosPauseActionTypeId) { - if (!m_sonosSystem->GetPlayer()->Pause()) { - return Device::DeviceErrorHardwareFailure; - } + if (action.actionTypeId() == sonosGroupStopActionTypeId) { + sonos->stop(); return Device::DeviceErrorNoError; } - if (action.actionTypeId() == sonosStopActionTypeId) { - if (!m_sonosSystem->GetPlayer()->Stop()) { - return Device::DeviceErrorHardwareFailure; - } - return Device::DeviceErrorNoError; - } + if (action.actionTypeId() == sonosGroupMuteActionTypeId) { + bool mute = action.param(sonosGroupMuteActionMuteParamTypeId).value().toBool(); - if (action.actionTypeId() == sonosMuteActionTypeId) { - bool mute = action.param(sonosMuteActionMuteParamTypeId).value().toBool(); - - SONOS::ZonePtr pl = m_sonosSystem->GetConnectedZone(); - for (SONOS::Zone::iterator ip = pl->begin(); ip != pl->end(); ++ip) { - if (!m_sonosSystem->GetPlayer()->SetMute((*ip)->GetUUID(), mute)) { - qWarning(dcSonos()) << "Could not set mute state for" << (*ip)->GetHost().c_str(); - return Device::DeviceErrorHardwareFailure; - } - } + sonos->setGroupMute() return Device::DeviceErrorNoError; } diff --git a/sonos/devicepluginsonos.h b/sonos/devicepluginsonos.h index 2b4e9377..3fb8f1da 100644 --- a/sonos/devicepluginsonos.h +++ b/sonos/devicepluginsonos.h @@ -48,7 +48,9 @@ public: Device::DeviceError executeAction(Device *device, const Action &action) override; private: - Sonos *sonos = nullptr; + PluginTimer *m_pluginTimer = nullptr; + QHash m_sonosConnections; + private slots: void onPluginTimer(); diff --git a/sonos/devicepluginsonos.json b/sonos/devicepluginsonos.json index 7992e42a..68daca04 100644 --- a/sonos/devicepluginsonos.json +++ b/sonos/devicepluginsonos.json @@ -11,12 +11,19 @@ "deviceClasses": [ { "id": "22df416d-7732-44f1-b6b9-e41296211178", - "name": "sonosGroup", - "displayName": "Sonos group", - "interfaces": ["gateway", "connectable"], - "createMethods": ["discovery"], - "paramTypes": [ + "name": "sonosConnection", + "displayName": "Sonos connection", + "interfaces": ["gateway"], + "createMethods": ["user"], + "setupMethod": "userandpassword", + "stateTypes": [ { + "id": "5aa4360c-61de-47d0-a72e-a19d57712e1c", + "name": "connected", + "displayName": "connected", + "displayNameEvent": "connected changed", + "defaultValue": false, + "type": "bool" } ] }, @@ -29,8 +36,8 @@ "paramTypes": [ { "id": "defc44cd-2ffb-4af1-b348-d6a3474c7515", - "name": "zoneName", - "displayName": "Zone name", + "name": "groupId", + "displayName": "Group id", "type" : "QString" } ], diff --git a/sonos/oauth.cpp b/sonos/oauth.cpp new file mode 100644 index 00000000..2c34c2b9 --- /dev/null +++ b/sonos/oauth.cpp @@ -0,0 +1,221 @@ +#include "oauth.h" +#include "extern-plugininfo.h" + +#include +#include +#include + +OAuth::OAuth(QString clientId, QObject *parent) : + QObject(parent), + m_clientId(clientId), + m_authenticated(false) +{ + m_networkManager = new QNetworkAccessManager(this); + connect(m_networkManager, &QNetworkAccessManager::finished, this, &OAuth::replyFinished); + + m_timer = new QTimer(this); + m_timer->setSingleShot(false); + + connect(m_timer, &QTimer::timeout, this, &OAuth::refreshTimeout); +} + +QUrl OAuth::url() const +{ + return m_url; +} + +void OAuth::setUrl(const QUrl &url) +{ + m_url = url; +} + +QUrlQuery OAuth::query() const +{ + return m_query; +} + +void OAuth::setQuery(const QUrlQuery &query) +{ + m_query = query; +} + +QString OAuth::clientId() const +{ + return m_clientId; +} + +void OAuth::setClientId(const QString &clientId) +{ + m_clientId = clientId; +} + +QString OAuth::scope() const +{ + return m_scope; +} + +void OAuth::setScope(const QString &scope) +{ + m_scope = scope; +} + +QString OAuth::authorizationCode() const +{ + return m_token; +} + +QString OAuth::bearerToken() const +{ + return m_token; +} + +bool OAuth::authenticated() const +{ + return m_authenticated; +} + +void OAuth::startAuthentication() +{ + qCDebug(dcSonos) << "Start authentication"; + + QUrlQuery query; + query.addQueryItem("client_id", m_clientId); + query.addQueryItem("redirect_uri", m_redirectUri); + query.addQueryItem("response_type", "code"); + query.addQueryItem("scope", m_scope); + m_state = QUuid().toByteArray(); + query.addQueryItem("state", m_state); + setQuery(query); + + QNetworkRequest request(m_url); + request.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded; charset=UTF-8"); + m_tokenRequests.append(m_networkManager->post(request, m_query.toString().toUtf8())); +} + +void OAuth::setAuthenticated(const bool &authenticated) +{ + if (authenticated) { + qCDebug(dcSonos()) << "Authenticated successfully"; + } else { + m_timer->stop(); + qCWarning(dcSonos) << "Authentication failed"; + } + m_authenticated = authenticated; + emit authenticationChanged(); +} + +void OAuth::setToken(const QString &token) +{ + m_token = token; + emit tokenChanged(); +} + +void OAuth::replyFinished(QNetworkReply *reply) +{ + int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); + reply->deleteLater(); + // token request + if (m_tokenRequests.contains(reply)) { + + QByteArray data = reply->readAll(); + m_tokenRequests.removeAll(reply); + + // check HTTP status code + if (status != 200) { + qCWarning(dcSonos) << "Request token reply HTTP error:" << status << reply->errorString(); + qCWarning(dcSonos) << data; + setAuthenticated(false); + return; + } + + // check JSON + QJsonParseError error; + QJsonDocument jsonDoc = QJsonDocument::fromJson(data, &error); + if (error.error != QJsonParseError::NoError) { + qCWarning(dcSonos) << "Request token reply JSON error:" << error.errorString(); + setAuthenticated(false); + return; + } + + if (!jsonDoc.toVariant().toMap().contains("code")) { + qCWarning(dcSonos) << "Could not get code" << jsonDoc.toJson(); + setAuthenticated(false); + return; + } + + if (!jsonDoc.toVariant().toMap().contains("state")) { + qCWarning(dcSonos) << "Could not get state" << jsonDoc.toJson(); + return; + } + + if (jsonDoc.toVariant().toMap().value("state").toString() != m_state) { + qCWarning(dcSonos) << "State doesn't match. Expected:" << m_state << "Received:" << jsonDoc.toVariant().toMap().value("state").toString(); + } + + setToken(jsonDoc.toVariant().toMap().value("code").toString()); + setAuthenticated(true); + + if (jsonDoc.toVariant().toMap().contains("expires_in") && jsonDoc.toVariant().toMap().contains("refresh_token")) { + int expireTime = jsonDoc.toVariant().toMap().value("expires_in").toInt(); + m_refreshToken = jsonDoc.toVariant().toMap().value("refresh_token").toString(); + qCDebug(dcSonos) << "Token will be refreshed in" << expireTime << "[s]"; + m_timer->start((expireTime - 20) * 1000); + } + + } else if (m_refreshTokenRequests.contains(reply)) { + + QByteArray data = reply->readAll(); + m_refreshTokenRequests.removeAll(reply); + + // check HTTP status code + if (status != 200) { + qCWarning(dcSonos) << "Refresh token reply HTTP error:" << status << reply->errorString(); + qCWarning(dcSonos) << data; + setAuthenticated(false); + return; + } + + // check JSON + QJsonParseError error; + QJsonDocument jsonDoc = QJsonDocument::fromJson(data, &error); + if (error.error != QJsonParseError::NoError) { + qCWarning(dcSonos) << "Refresh token reply JSON error:" << error.errorString(); + setAuthenticated(false); + return; + } + + if (!jsonDoc.toVariant().toMap().contains("access_token")) { + qCWarning(dcSonos) << "Could not get access token after refresh" << jsonDoc.toJson(); + setAuthenticated(false); + return; + } + + + setToken(jsonDoc.toVariant().toMap().value("access_token").toString()); + qCDebug(dcSonos) << "Token refreshed successfully"; + + if (jsonDoc.toVariant().toMap().contains("expires_in") && jsonDoc.toVariant().toMap().contains("refresh_token")) { + int expireTime = jsonDoc.toVariant().toMap().value("expires_in").toInt(); + m_refreshToken = jsonDoc.toVariant().toMap().value("refresh_token").toString(); + qCDebug(dcSonos) << "Token will be refreshed in" << expireTime << "[s]"; + m_timer->start((expireTime - 20) * 1000); + } + + if (!authenticated()) + setAuthenticated(true); + } +} + +void OAuth::refreshTimeout() +{ + qCDebug(dcSonos) << "Refresh authentication token for"; + + QUrlQuery query; + query.addQueryItem("grant_type", "refresh_token"); + query.addQueryItem("refresh_token", m_refreshToken); + query.addQueryItem("client_id", m_clientId); + + QNetworkRequest request(m_url); + request.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded; charset=UTF-8"); + m_refreshTokenRequests.append(m_networkManager->post(request, query.toString().toUtf8())); +} diff --git a/sonos/oauth.h b/sonos/oauth.h new file mode 100644 index 00000000..205eda51 --- /dev/null +++ b/sonos/oauth.h @@ -0,0 +1,71 @@ +#ifndef OAUTH_H +#define OAUTH_H + + +#include +#include +#include +#include +#include +#include +#include + +class OAuth : public QObject +{ + Q_OBJECT + +public: + explicit OAuth(QString clientId, QObject *parent = nullptr); + + QUrl url() const; + void setUrl(const QUrl &url); + + QUrlQuery query() const; + void setQuery(const QUrlQuery &query); + + QString clientId() const; + void setClientId(const QString &clientId); + + QString scope() const; + void setScope(const QString &scope); + + QByteArray authorizationCode() const; + QByteArray bearerToken() const; + + bool authenticated() const; + + void startAuthentication(); + +private: + + QNetworkAccessManager *m_networkManager; + QTimer *m_timer; + QList m_tokenRequests; + QList m_refreshTokenRequests; + + QUrl m_url; + QUrlQuery m_query; + QString m_clientId; + QString m_scope; + QString m_state; + QString m_redirectUri; + QString m_responseType; + + QString m_token; + QString m_refreshToken; + + bool m_authenticated; + + void setAuthenticated(const bool &authenticated); + void setToken(const QString &token); + +private slots: + void replyFinished(QNetworkReply *reply); + void refreshTimeout(); + +signals: + void authenticationChanged(); + void tokenChanged(); +}; + +#endif // OAUTH_H diff --git a/sonos/sonos.cpp b/sonos/sonos.cpp index 919c9c47..3263085b 100644 --- a/sonos/sonos.cpp +++ b/sonos/sonos.cpp @@ -22,26 +22,33 @@ #include "sonos.h" #include "extern-plugininfo.h" +#include "network/networkaccessmanager.h" #include -Sonos::Sonos(QObject *parent) +Sonos::Sonos(QByteArray apiKey, QObject *parent) : + QObject(parent), + m_apiKey(apiKey) { } void Sonos::authenticate(const QString &username, const QString &password) { - //get oauth autherisation + Q_UNUSED(username) + Q_UNUSED(password) - //get accesst token + m_OAuth = new OAuth(m_apiKey, this); + m_OAuth->setUrl(QUrl(m_baseAuthorizationUrl)); + m_OAuth->setScope("playback-control-all"); + m_OAuth->startAuthentication(); } void Sonos::getHouseholds() { QNetworkRequest request; request.setHeader(QNetworkRequest::KnownHeaders::ContentTypeHeader, "application/json"); - request.setRawHeader("Authorization", "Bearer" + m_bearerToken); - request.setUrl(m_baseControlUrl + "/households"); + request.setRawHeader("Authorization", "Bearer" + m_OAuth->bearerToken()); + request.setUrl(QUrl(m_baseControlUrl + "/households")); QNetworkReply *reply = QNetworkAccessManager.get(request); connect(reply, &QNetworkReply::finished, this [this] { int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); @@ -61,12 +68,3 @@ void Sonos::getHouseholds() }); } -void Sonos::getPlayerVolume(int playerId) -{ - QNetworkRequest request; - - QJsonObject object; - object. -} - - diff --git a/sonos/sonos.h b/sonos/sonos.h index 832ab065..8ff3e0c4 100644 --- a/sonos/sonos.h +++ b/sonos/sonos.h @@ -27,6 +27,7 @@ #include #include "devices/device.h" +#include "oauth.h" class Sonos : public QObject { @@ -135,8 +136,7 @@ public: { QString name; ArtistObject artist; - QString - + //TODO }; struct ContainerObject @@ -150,6 +150,8 @@ public: }; explicit Sonos(QByteArray apiKey, QObject *parent = nullptr); + + void setApiKey(QByteArray apiKey); void authenticate(const QString &username, const QString &password); void getHouseholds(); @@ -201,14 +203,17 @@ public: void setPlayerSettings(); private: - QUrl m_baseAuthorizationUrl = "api.sonos.com/login/v3/oauth"; - QUrl m_baseControlUrl = "api.ws.sonos.com/control/api/v1"; + QByteArray m_baseAuthorizationUrl = "api.sonos.com/login/v3/oauth"; + QByteArray m_baseControlUrl = "api.ws.sonos.com/control/api/v1"; + QByteArray m_apiKey; + + OAuth *m_OAuth = nullptr; private slots: signals: - void authenticationSuccessfull(); + void authenticationFinished(); void authenticationFailed(const QString &reason); diff --git a/sonos/sonos.pro b/sonos/sonos.pro index a107f02e..7043560f 100644 --- a/sonos/sonos.pro +++ b/sonos/sonos.pro @@ -7,9 +7,11 @@ TARGET = $$qtLibraryTarget(nymea_devicepluginsonos) SOURCES += \ devicepluginsonos.cpp \ sonos.cpp \ + oauth.cpp HEADERS += \ devicepluginsonos.h \ sonos.h \ + oauth.h From 34b97e89d4bf565cabab23141e9f920d9ec8e83c Mon Sep 17 00:00:00 2001 From: nymea Date: Thu, 29 Aug 2019 21:10:32 +0200 Subject: [PATCH 08/22] added new nymea auth method --- sonos/devicepluginsonos.cpp | 164 +++++++++++++++++++++--------- sonos/devicepluginsonos.h | 8 +- sonos/devicepluginsonos.json | 6 +- sonos/sonos.cpp | 190 ++++++++++++++++++++++++++++++++--- sonos/sonos.h | 17 +--- sonos/sonos.pro | 2 - 6 files changed, 304 insertions(+), 83 deletions(-) diff --git a/sonos/devicepluginsonos.cpp b/sonos/devicepluginsonos.cpp index 24f61306..bfdbabca 100644 --- a/sonos/devicepluginsonos.cpp +++ b/sonos/devicepluginsonos.cpp @@ -22,10 +22,13 @@ #include "devicepluginsonos.h" #include "devices/device.h" +#include "network/networkaccessmanager.h" #include "plugininfo.h" #include #include +#include +#include DevicePluginSonos::DevicePluginSonos() { @@ -45,23 +48,13 @@ Device::DeviceSetupStatus DevicePluginSonos::setupDevice(Device *device) } if (device->deviceClassId() == sonosConnectionDeviceClassId) { - - Sonos *sonos = new Sonos("0a8f6d44-d9d1-4474-bcfa-cfb41f8b66e8", this); + qCDebug(dcSonos()) << "Sonos OAuth setup complete"; + Sonos *sonos = new Sonos(hardwareManager()->networkManager(), "0a8f6d44-d9d1-4474-bcfa-cfb41f8b66e8", this); pluginStorage()->beginGroup(device->id().toString()); QString username = pluginStorage()->value("username").toString(); QString password = pluginStorage()->value("password").toString(); pluginStorage()->endGroup(); - - sonos->authenticate(username, password); - - m_sonosConnections.insert(device->id(), sonos); - connect(sonos, &Sonos::authenticationFinished, this, [this, sonos](bool success){ - if (success) { - } else { - qCWarning(dcSonos()) << "Cannot authenticate to Sonos api"; - } - }); - + m_sonosConnections.insert(device, sonos); } if (device->deviceClassId() == sonosGroupDeviceClassId) { @@ -71,6 +64,88 @@ Device::DeviceSetupStatus DevicePluginSonos::setupDevice(Device *device) return Device::DeviceSetupStatusSuccess; } +DevicePairingInfo DevicePluginSonos::pairDevice(DevicePairingInfo &devicePairingInfo) +{ + + if (devicePairingInfo.deviceClassId() == sonosConnectionDeviceClassId) { + QString clientId = "b15cbf8c-a39c-47aa-bd93-635a96e9696c"; + QString clientSecret = "c086ba71-e562-430b-a52f-867c6482fd11"; + + QUrl url("https://api.sonos.com/login/v3/oauth"); + QUrlQuery queryParams; + queryParams.addQueryItem("client_id", clientId); + queryParams.addQueryItem("redirect_uri", "https://127.0.0.1:8888"); + queryParams.addQueryItem("response_type", "code"); + queryParams.addQueryItem("scope", "playback-control-all"); + queryParams.addQueryItem("state", QUuid::createUuid().toString()); + url.setQuery(queryParams); + + qCDebug(dcSonos()) << "Sonos url:" << url; + + devicePairingInfo.setOAuthUrl(url); + devicePairingInfo.setStatus(Device::DeviceErrorNoError); + return devicePairingInfo; + } + + qCWarning(dcSonos()) << "Unhandled pairing metod!"; + devicePairingInfo.setStatus(Device::DeviceErrorCreationMethodNotSupported); + return devicePairingInfo; +} + +DevicePairingInfo DevicePluginSonos::confirmPairing(DevicePairingInfo &devicePairingInfo, const QString &username, const QString &secret) +{ + Q_UNUSED(username); + qCDebug(dcSonos()) << "Confirm pairing"; + + if (devicePairingInfo.deviceClassId() == sonosConnectionDeviceClassId) { + qCDebug(dcSonos()) << "Secret is" << secret; + QUrl url(secret); + QUrlQuery query(url); + qCDebug(dcSonos()) << "Acess code is:" << query.queryItemValue("code"); + + QString accessCode = query.queryItemValue("code"); + + // Obtaining access token + url = QUrl("https://api.sonos.com/login/v3/oauth/access"); + query.clear(); + query.addQueryItem("grant_type", "authorization_code"); + query.addQueryItem("code", accessCode); + query.addQueryItem("redirect_uri", "https%3A%2F%2F127.0.0.1%3A8888"); + url.setQuery(query); + + QNetworkRequest request(url); + request.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded;charset=utf-8"); + + QByteArray clientId = "b15cbf8c-a39c-47aa-bd93-635a96e9696c"; + QByteArray clientSecret = "c086ba71-e562-430b-a52f-867c6482fd11"; + + QByteArray auth = QByteArray(clientId + ':' + clientSecret).toBase64(QByteArray::Base64Encoding | QByteArray::KeepTrailingEquals); + request.setRawHeader("Authorization", QString("Basic %1").arg(QString(auth)).toUtf8()); + + QNetworkReply *reply = hardwareManager()->networkManager()->post(request, QByteArray()); + connect(reply, &QNetworkReply::finished, this, [this, reply, devicePairingInfo](){ + reply->deleteLater(); + + QJsonDocument jsonDoc = QJsonDocument::fromJson(reply->readAll()); + qCDebug(dcSonos()) << "Sonos accessToken reply:" << this << reply->error() << reply->errorString() << jsonDoc.toJson(); + qCDebug(dcSonos()) << "Access token:" << jsonDoc.toVariant().toMap().value("access_token").toString(); + qCDebug(dcSonos()) << "expires at" << QDateTime::currentDateTime().addSecs(jsonDoc.toVariant().toMap().value("expires_in").toInt()).toString(); + qCDebug(dcSonos()) << "Refresh token:" << jsonDoc.toVariant().toMap().value("refresh_token").toString(); + DevicePairingInfo info(devicePairingInfo); + info.setStatus(Device::DeviceErrorNoError); + emit pairingFinished(info); + }); + + devicePairingInfo.setStatus(Device::DeviceErrorAsync); + return devicePairingInfo; + } + + + qCWarning(dcSonos()) << "Invalid deviceclassId -> no pairing possible with this device"; + devicePairingInfo.setStatus(Device::DeviceErrorHardwareFailure); + return devicePairingInfo; +} + void DevicePluginSonos::postSetupDevice(Device *device) { if (device->deviceClassId() == sonosConnectionDeviceClassId) { @@ -82,6 +157,16 @@ void DevicePluginSonos::postSetupDevice(Device *device) } } + +void DevicePluginSonos::startMonitoringAutoDevices() +{ + foreach (Device *device, myDevices()) { + if (device->deviceClassId() == sonosGroupDeviceClassId) { + return; // We already have a Auto Mock device... do nothing. + } + } +} + void DevicePluginSonos::deviceRemoved(Device *device) { qCDebug(dcSonos) << "Delete " << device->name(); @@ -94,8 +179,8 @@ Device::DeviceError DevicePluginSonos::executeAction(Device *device, const Actio { Q_UNUSED(action) if (device->deviceClassId() == sonosGroupDeviceClassId) { - Sonos *sonos = m_sonosConnections.value(device->parentId()); - int groupId = device->paramValue(sonosGroupDe) + Sonos *sonos = m_sonosConnections.value(device); + //int groupId = device->paramValue(sonosGroupDe) if (!sonos) return Device::DeviceErrorInvalidParameter; @@ -131,41 +216,33 @@ Device::DeviceError DevicePluginSonos::executeAction(Device *device, const Actio } if (action.actionTypeId() == sonosGroupStopActionTypeId) { - sonos->stop(); + //sonos->stop(); return Device::DeviceErrorNoError; } if (action.actionTypeId() == sonosGroupMuteActionTypeId) { - bool mute = action.param(sonosGroupMuteActionMuteParamTypeId).value().toBool(); + //bool mute = action.param(sonosGroupMuteActionMuteParamTypeId).value().toBool(); - sonos->setGroupMute() + //sonos->setGroupMute(); return Device::DeviceErrorNoError; } - if (action.actionTypeId() == sonosSkipNextActionTypeId) { - if (!m_sonosSystem->GetPlayer()->Next()) { + if (action.actionTypeId() == sonosGroupSkipNextActionTypeId) { + /*if (!m_sonosSystem->GetPlayer()->Next()) { return Device::DeviceErrorHardwareFailure; - } + }*/ return Device::DeviceErrorNoError; } - if (action.actionTypeId() == sonosSkipBackActionTypeId) { - if(!m_sonosSystem->GetPlayer()->Previous()) { + if (action.actionTypeId() == sonosGroupSkipBackActionTypeId) { + /* if(!m_sonosSystem->GetPlayer()->Previous()) { return Device::DeviceErrorHardwareFailure; - } + }*/ return Device::DeviceErrorNoError; } - if (action.actionTypeId() == sonosSkipBackActionTypeId) { - int volume = action.param(sonosVolumeActionVolumeParamTypeId).value().toInt(); - - SONOS::ZonePtr pl = m_sonosSystem->GetConnectedZone(); - for (SONOS::Zone::iterator ip = pl->begin(); ip != pl->end(); ++ip) { - if (!m_sonosSystem->GetPlayer()->SetVolume((*ip)->GetUUID(), volume)) { - qWarning(dcSonos()) << "Could not set volume for" << (*ip)->GetHost().c_str(); - return Device::DeviceErrorHardwareFailure; - } - } + if (action.actionTypeId() == sonosGroupSkipBackActionTypeId) { + //int volume = action.param(sonosVolumeActionVolumeParamTypeId).value().toInt(); return Device::DeviceErrorNoError; } return Device::DeviceErrorActionTypeNotFound; @@ -177,18 +254,11 @@ void DevicePluginSonos::onPluginTimer() { } -void DevicePluginSonos::handleEventCB(void* handle) +void DevicePluginSonos::onConnectionChanged() { - Q_UNUSED(handle); - /*unsigned char mask = m_sonosSystem->LastEvents(); - if ((mask & SONOS::SVCEvent_TransportChanged)) - qDebug(dcSonos()) << "Event Transport changed"; - if ((mask & SONOS::SVCEvent_AlarmClockChanged)) - qDebug(dcSonos()) << "Alarm clock changed"; - if ((mask & SONOS::SVCEvent_ZGTopologyChanged)) - qDebug(dcSonos()) << "ZG Topology changed"; - if ((mask & SONOS::SVCEvent_ContentDirectoryChanged)) - qDebug(dcSonos()) << "Content directory changed"; - if ((mask & SONOS::SVCEvent_RenderingControlChanged)) - qDebug(dcSonos()) << "Rendering control changed";*/ + Sonos *sonos = static_cast(sender()); + Device *device = m_sonosConnections.key(sonos); + device->setStateValue(sonosConnectionConnectedStateTypeId, false); //TODO + + //TODO set all groups } diff --git a/sonos/devicepluginsonos.h b/sonos/devicepluginsonos.h index 3fb8f1da..db84374b 100644 --- a/sonos/devicepluginsonos.h +++ b/sonos/devicepluginsonos.h @@ -42,15 +42,17 @@ public: ~DevicePluginSonos() override; Device::DeviceSetupStatus setupDevice(Device *device) override; + DevicePairingInfo pairDevice(DevicePairingInfo &devicePairingInfo) override; + DevicePairingInfo confirmPairing(DevicePairingInfo &devicePairingInfo, const QString &username, const QString &secret) override; + void postSetupDevice(Device *device) override; + void startMonitoringAutoDevices() override; void deviceRemoved(Device *device) override; - Device::DeviceError discoverDevices(const DeviceClassId &deviceClassId, const ParamList ¶ms) override; Device::DeviceError executeAction(Device *device, const Action &action) override; private: PluginTimer *m_pluginTimer = nullptr; - QHash m_sonosConnections; - + QHash m_sonosConnections; private slots: void onPluginTimer(); diff --git a/sonos/devicepluginsonos.json b/sonos/devicepluginsonos.json index 68daca04..a59bd854 100644 --- a/sonos/devicepluginsonos.json +++ b/sonos/devicepluginsonos.json @@ -15,7 +15,7 @@ "displayName": "Sonos connection", "interfaces": ["gateway"], "createMethods": ["user"], - "setupMethod": "userandpassword", + "setupMethod": "oauth", "stateTypes": [ { "id": "5aa4360c-61de-47d0-a72e-a19d57712e1c", @@ -28,11 +28,11 @@ ] }, { - "id": "22df416d-7732-44f1-b6b9-e41296211178", + "id": "72d9332b-2b25-4136-87a6-e534eae4cc80", "name": "sonosGroup", "displayName": "Sonos group", "interfaces": ["extendedvolumecontroller", "mediametadataprovider", "shufflerepeat", "connectable"], - "createMethods": ["discovery"], + "createMethods": ["auto"], "paramTypes": [ { "id": "defc44cd-2ffb-4af1-b348-d6a3474c7515", diff --git a/sonos/sonos.cpp b/sonos/sonos.cpp index 3263085b..20cf52dc 100644 --- a/sonos/sonos.cpp +++ b/sonos/sonos.cpp @@ -22,35 +22,30 @@ #include "sonos.h" #include "extern-plugininfo.h" -#include "network/networkaccessmanager.h" #include -Sonos::Sonos(QByteArray apiKey, QObject *parent) : +Sonos::Sonos(NetworkAccessManager *networkmanager, const QByteArray &accessToken, QObject *parent) : QObject(parent), - m_apiKey(apiKey) + m_accessToken(accessToken), + m_networkManager(networkmanager) { } -void Sonos::authenticate(const QString &username, const QString &password) +void Sonos::setAccessToken(const QByteArray &accessToken) { - Q_UNUSED(username) - Q_UNUSED(password) - - m_OAuth = new OAuth(m_apiKey, this); - m_OAuth->setUrl(QUrl(m_baseAuthorizationUrl)); - m_OAuth->setScope("playback-control-all"); - m_OAuth->startAuthentication(); + m_accessToken = accessToken; } void Sonos::getHouseholds() { QNetworkRequest request; request.setHeader(QNetworkRequest::KnownHeaders::ContentTypeHeader, "application/json"); - request.setRawHeader("Authorization", "Bearer" + m_OAuth->bearerToken()); + request.setRawHeader("Authorization", "Bearer" + m_accessToken); request.setUrl(QUrl(m_baseControlUrl + "/households")); - QNetworkReply *reply = QNetworkAccessManager.get(request); - connect(reply, &QNetworkReply::finished, this [this] { + QNetworkReply *reply = m_networkManager->get(request); + connect(reply, &QNetworkReply::finished, this, [reply, this] { + reply->deleteLater(); int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); // Check HTTP status code @@ -58,13 +53,176 @@ void Sonos::getHouseholds() qCWarning(dcSonos()) << "Request error:" << status << reply->errorString(); return; } - QJsonDocument data = reply->readAll(); + + qDebug(dcSonos()) << "Received response from Sonos" << reply->readAll(); + /*QJsonDocument data = reply->readAll(); if (!data.isObject()) return; QList households; - emit householdObjectsReceived(households); + emit householdObjectsReceived(households);*/ }); } +void Sonos::cancelAudioClip() +{ + +} + +void Sonos::loadAudioClip() +{ + +} + +void Sonos::getFavorites() +{ + +} + +void Sonos::loadFavorite() +{ + +} + +void Sonos::getGroups() +{ + +} + +void Sonos::createGroup() +{ + +} + +void Sonos::modifyGroupMembers() +{ + +} + +void Sonos::setGroupMembers() +{ + +} + +void Sonos::getGroupVolume(int groupId) +{ + Q_UNUSED(groupId) +} + +void Sonos::setGroupVolume(int groupId, int volume) +{ + Q_UNUSED(groupId) + Q_UNUSED(volume) +} + +void Sonos::setGroupMute(int groupId, bool mute) +{ + Q_UNUSED(groupId) + Q_UNUSED(mute) +} + +void Sonos::setGroupRelativeVolume(int groupId, int volumeDelta) +{ + Q_UNUSED(groupId) + Q_UNUSED(volumeDelta) +} + +void Sonos::getPlaybackStatus() +{ + +} + +void Sonos::loadLineIn() +{ + +} + +void Sonos::play() +{ + +} + +void Sonos::pause() +{ + +} + +void Sonos::seek() +{ + +} + +void Sonos::seekRelative() +{ + +} + +void Sonos::setPlayModes() +{ + +} + +void Sonos::skipToNextTrack() +{ + +} + +void Sonos::skipToPreviousTrack() +{ + +} + +void Sonos::togglePlayPause() +{ + +} + +void Sonos::getPlayerVolume(int playerId) +{ + Q_UNUSED(playerId) +} + +void Sonos::setPlayerVolume(int playerId, int volume) +{ + Q_UNUSED(playerId) + Q_UNUSED(volume) +} + +void Sonos::setPlayerRelativeVolume(int playerId, int volumeDelta) +{ + Q_UNUSED(playerId) + Q_UNUSED(volumeDelta) +} + +void Sonos::setPlayerMute(int playerId, bool mute) +{ + Q_UNUSED(playerId) + Q_UNUSED(mute) +} + +void Sonos::getPlaylist() +{ + +} + +void Sonos::getPlaylists() +{ + +} + +void Sonos::loadPlaylist() +{ + +} + +void Sonos::getPlayerSettings() +{ + +} + +void Sonos::setPlayerSettings() +{ + +} + diff --git a/sonos/sonos.h b/sonos/sonos.h index 8ff3e0c4..4d98478a 100644 --- a/sonos/sonos.h +++ b/sonos/sonos.h @@ -24,10 +24,9 @@ #define SONOS_H #include -#include +#include "network/networkaccessmanager.h" #include "devices/device.h" -#include "oauth.h" class Sonos : public QObject { @@ -149,10 +148,9 @@ public: //tags enum }; - explicit Sonos(QByteArray apiKey, QObject *parent = nullptr); + explicit Sonos(NetworkAccessManager *networkManager, const QByteArray &accessToken, QObject *parent = nullptr); - void setApiKey(QByteArray apiKey); - void authenticate(const QString &username, const QString &password); + void setAccessToken(const QByteArray &accessToken); void getHouseholds(); @@ -205,19 +203,14 @@ public: private: QByteArray m_baseAuthorizationUrl = "api.sonos.com/login/v3/oauth"; QByteArray m_baseControlUrl = "api.ws.sonos.com/control/api/v1"; - QByteArray m_apiKey; + QByteArray m_accessToken; - OAuth *m_OAuth = nullptr; + NetworkAccessManager *m_networkManager = nullptr; private slots: signals: - void authenticationFinished(); - void authenticationFailed(const QString &reason); - - -public slots: }; diff --git a/sonos/sonos.pro b/sonos/sonos.pro index 7043560f..a107f02e 100644 --- a/sonos/sonos.pro +++ b/sonos/sonos.pro @@ -7,11 +7,9 @@ TARGET = $$qtLibraryTarget(nymea_devicepluginsonos) SOURCES += \ devicepluginsonos.cpp \ sonos.cpp \ - oauth.cpp HEADERS += \ devicepluginsonos.h \ sonos.h \ - oauth.h From 4254d3cb3cbfaea2041a8c8fda48da9c58efab92 Mon Sep 17 00:00:00 2001 From: nymea Date: Sun, 1 Sep 2019 21:41:10 +0200 Subject: [PATCH 09/22] added plugin Storage for tokens --- sonos/devicepluginsonos.cpp | 180 +++++++++++++++++++++++------------ sonos/devicepluginsonos.h | 10 ++ sonos/devicepluginsonos.json | 2 + sonos/sonos.cpp | 147 ++++++++++++++++++---------- sonos/sonos.h | 89 ++++++++++------- 5 files changed, 283 insertions(+), 145 deletions(-) diff --git a/sonos/devicepluginsonos.cpp b/sonos/devicepluginsonos.cpp index bfdbabca..bba6e3a3 100644 --- a/sonos/devicepluginsonos.cpp +++ b/sonos/devicepluginsonos.cpp @@ -45,15 +45,23 @@ DevicePluginSonos::~DevicePluginSonos() Device::DeviceSetupStatus DevicePluginSonos::setupDevice(Device *device) { if (!m_pluginTimer) { - + hardwareManager()->pluginTimerManager()->registerTimer(1); } + + if(!m_tokenRefreshTimer) { + m_tokenRefreshTimer = new QTimer(this); + m_tokenRefreshTimer->setSingleShot(false); + connect(m_tokenRefreshTimer, &QTimer::timeout, this, &DevicePluginSonos::onRefreshTimeout); + } + if (device->deviceClassId() == sonosConnectionDeviceClassId) { qCDebug(dcSonos()) << "Sonos OAuth setup complete"; - Sonos *sonos = new Sonos(hardwareManager()->networkManager(), "0a8f6d44-d9d1-4474-bcfa-cfb41f8b66e8", this); + pluginStorage()->beginGroup(device->id().toString()); - QString username = pluginStorage()->value("username").toString(); - QString password = pluginStorage()->value("password").toString(); + QByteArray accessToken = pluginStorage()->value("access_token").toByteArray(); pluginStorage()->endGroup(); + + Sonos *sonos = new Sonos(hardwareManager()->networkManager(), accessToken, this); m_sonosConnections.insert(device, sonos); } @@ -98,49 +106,66 @@ DevicePairingInfo DevicePluginSonos::confirmPairing(DevicePairingInfo &devicePai qCDebug(dcSonos()) << "Confirm pairing"; if (devicePairingInfo.deviceClassId() == sonosConnectionDeviceClassId) { - qCDebug(dcSonos()) << "Secret is" << secret; - QUrl url(secret); - QUrlQuery query(url); - qCDebug(dcSonos()) << "Acess code is:" << query.queryItemValue("code"); + qCDebug(dcSonos()) << "Secret is" << secret; + QUrl url(secret); + QUrlQuery query(url); + qCDebug(dcSonos()) << "Acess code is:" << query.queryItemValue("code"); - QString accessCode = query.queryItemValue("code"); + QString accessCode = query.queryItemValue("code"); - // Obtaining access token - url = QUrl("https://api.sonos.com/login/v3/oauth/access"); - query.clear(); - query.addQueryItem("grant_type", "authorization_code"); - query.addQueryItem("code", accessCode); - query.addQueryItem("redirect_uri", "https%3A%2F%2F127.0.0.1%3A8888"); - url.setQuery(query); + // Obtaining access token + url = QUrl("https://api.sonos.com/login/v3/oauth/access"); + query.clear(); + query.addQueryItem("grant_type", "authorization_code"); + query.addQueryItem("code", accessCode); + query.addQueryItem("redirect_uri", "https%3A%2F%2F127.0.0.1%3A8888"); + url.setQuery(query); - QNetworkRequest request(url); - request.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded;charset=utf-8"); + QNetworkRequest request(url); + request.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded;charset=utf-8"); - QByteArray clientId = "b15cbf8c-a39c-47aa-bd93-635a96e9696c"; - QByteArray clientSecret = "c086ba71-e562-430b-a52f-867c6482fd11"; + QByteArray clientId = "b15cbf8c-a39c-47aa-bd93-635a96e9696c"; + QByteArray clientSecret = "c086ba71-e562-430b-a52f-867c6482fd11"; - QByteArray auth = QByteArray(clientId + ':' + clientSecret).toBase64(QByteArray::Base64Encoding | QByteArray::KeepTrailingEquals); - request.setRawHeader("Authorization", QString("Basic %1").arg(QString(auth)).toUtf8()); + QByteArray auth = QByteArray(clientId + ':' + clientSecret).toBase64(QByteArray::Base64Encoding | QByteArray::KeepTrailingEquals); + request.setRawHeader("Authorization", QString("Basic %1").arg(QString(auth)).toUtf8()); - QNetworkReply *reply = hardwareManager()->networkManager()->post(request, QByteArray()); - connect(reply, &QNetworkReply::finished, this, [this, reply, devicePairingInfo](){ - reply->deleteLater(); + QNetworkReply *reply = hardwareManager()->networkManager()->post(request, QByteArray()); + connect(reply, &QNetworkReply::finished, this, [this, reply, devicePairingInfo](){ + reply->deleteLater(); + DevicePairingInfo info(devicePairingInfo); - QJsonDocument jsonDoc = QJsonDocument::fromJson(reply->readAll()); - qCDebug(dcSonos()) << "Sonos accessToken reply:" << this << reply->error() << reply->errorString() << jsonDoc.toJson(); - qCDebug(dcSonos()) << "Access token:" << jsonDoc.toVariant().toMap().value("access_token").toString(); - qCDebug(dcSonos()) << "expires at" << QDateTime::currentDateTime().addSecs(jsonDoc.toVariant().toMap().value("expires_in").toInt()).toString(); - qCDebug(dcSonos()) << "Refresh token:" << jsonDoc.toVariant().toMap().value("refresh_token").toString(); - DevicePairingInfo info(devicePairingInfo); - info.setStatus(Device::DeviceErrorNoError); - emit pairingFinished(info); - }); + QJsonDocument jsonDoc = QJsonDocument::fromJson(reply->readAll()); + 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") ) { + info.setStatus(Device::DeviceErrorSetupFailed); + emit pairingFinished(info); + return; + } + qCDebug(dcSonos()) << "Access token:" << jsonDoc.toVariant().toMap().value("access_token").toString(); + QByteArray accessToken = jsonDoc.toVariant().toMap().value("access_token").toByteArray(); - devicePairingInfo.setStatus(Device::DeviceErrorAsync); - return devicePairingInfo; - } + qCDebug(dcSonos()) << "Refresh token:" << jsonDoc.toVariant().toMap().value("refresh_token").toString(); + QByteArray refreshToken = jsonDoc.toVariant().toMap().value("refresh_token").toByteArray(); + pluginStorage()->beginGroup(info.deviceId().toString()); + pluginStorage()->setValue("access_token", accessToken); + pluginStorage()->setValue("refresh_token", refreshToken); + pluginStorage()->endGroup(); + /*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(); + m_tokenRefreshTimer->start((expireTime - 20) * 1000); + }*/ + + info.setStatus(Device::DeviceErrorNoError); + emit pairingFinished(info); + }); + + devicePairingInfo.setStatus(Device::DeviceErrorAsync); + return devicePairingInfo; + } qCWarning(dcSonos()) << "Invalid deviceclassId -> no pairing possible with this device"; devicePairingInfo.setStatus(Device::DeviceErrorHardwareFailure); return devicePairingInfo; @@ -149,7 +174,8 @@ DevicePairingInfo DevicePluginSonos::confirmPairing(DevicePairingInfo &devicePai void DevicePluginSonos::postSetupDevice(Device *device) { if (device->deviceClassId() == sonosConnectionDeviceClassId) { - + Sonos *sonos = m_sonosConnections.value(device); + sonos->getHouseholds(); } if (device->deviceClassId() == sonosGroupDeviceClassId) { @@ -180,69 +206,59 @@ Device::DeviceError DevicePluginSonos::executeAction(Device *device, const Actio Q_UNUSED(action) if (device->deviceClassId() == sonosGroupDeviceClassId) { Sonos *sonos = m_sonosConnections.value(device); - //int groupId = device->paramValue(sonosGroupDe) + QByteArray groupId = device->paramValue(sonosGroupDeviceGroupIdParamTypeId).toByteArray(); + if (!sonos) return Device::DeviceErrorInvalidParameter; if (action.actionTypeId() == sonosGroupPlayActionTypeId) { - sonos->play(); + sonos->groupPlay(groupId); return Device::DeviceErrorAsync; } if (action.actionTypeId() == sonosGroupShuffleActionTypeId) { - + bool shuffle = action.param(sonosGroupShuffleActionShuffleParamTypeId).value().toBool(); + sonos->groupSetShuffle(groupId, shuffle); return Device::DeviceErrorAsync; } if (action.actionTypeId() == sonosGroupRepeatActionTypeId) { if (action.param(sonosGroupRepeatActionRepeatParamTypeId).value().toString() == "None") { - + sonos->groupSetRepeat(groupId, Sonos::RepeatModeNone); } else if (action.param(sonosGroupShuffleActionShuffleParamTypeId).value().toString() == "One") { - + sonos->groupSetRepeat(groupId, Sonos::RepeatModeOne); } else if (action.param(sonosGroupShuffleActionShuffleParamTypeId).value().toString() == "All") { - + sonos->groupSetRepeat(groupId, Sonos::RepeatModeAll); } else { return Device::DeviceErrorHardwareFailure; } - - return Device::DeviceErrorAsync; } if (action.actionTypeId() == sonosGroupPauseActionTypeId) { - sonos->pause(); + sonos->groupPause(groupId); return Device::DeviceErrorNoError; } if (action.actionTypeId() == sonosGroupStopActionTypeId) { - //sonos->stop(); + sonos->groupPause(groupId); return Device::DeviceErrorNoError; } if (action.actionTypeId() == sonosGroupMuteActionTypeId) { - //bool mute = action.param(sonosGroupMuteActionMuteParamTypeId).value().toBool(); - - //sonos->setGroupMute(); + bool mute = action.param(sonosGroupMuteActionMuteParamTypeId).value().toBool(); + sonos->setGroupMute(groupId, mute); return Device::DeviceErrorNoError; } if (action.actionTypeId() == sonosGroupSkipNextActionTypeId) { - /*if (!m_sonosSystem->GetPlayer()->Next()) { - return Device::DeviceErrorHardwareFailure; - }*/ + sonos->groupSkipToNextTrack(groupId); return Device::DeviceErrorNoError; } if (action.actionTypeId() == sonosGroupSkipBackActionTypeId) { - /* if(!m_sonosSystem->GetPlayer()->Previous()) { - return Device::DeviceErrorHardwareFailure; - }*/ - return Device::DeviceErrorNoError; - } - - if (action.actionTypeId() == sonosGroupSkipBackActionTypeId) { - //int volume = action.param(sonosVolumeActionVolumeParamTypeId).value().toInt(); + sonos->groupSkipToPreviousTrack(groupId); return Device::DeviceErrorNoError; } return Device::DeviceErrorActionTypeNotFound; @@ -262,3 +278,43 @@ void DevicePluginSonos::onConnectionChanged() //TODO set all groups } + +void DevicePluginSonos::onRefreshTimeout() +{ + qCDebug(dcSonos) << "Refresh authentication token"; + + QUrlQuery query; + query.addQueryItem("grant_type", "refresh_token"); + query.addQueryItem("refresh_token", m_sonosConnectionRefreshToken); + + QUrl url("https://api.sonos.com/login/v3/oauth"); + QNetworkRequest request(url); + request.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded; charset=UTF-8"); + QNetworkReply *reply = hardwareManager()->networkManager()->post(request, QByteArray()); + connect(reply, &QNetworkReply::finished, this, [this, reply](){ + reply->deleteLater(); + + QJsonDocument jsonDoc = QJsonDocument::fromJson(reply->readAll()); + qCDebug(dcSonos()) << "Sonos accessToken reply:" << this << reply->error() << reply->errorString() << jsonDoc.toJson(); + if(!jsonDoc.toVariant().toMap().contains("access_token")) { + return; + } + qCDebug(dcSonos()) << "Access token:" << jsonDoc.toVariant().toMap().value("access_token").toString(); + m_sonosConnectionAccessToken = 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()) << "expires at" << QDateTime::currentDateTime().addSecs(expireTime).toString(); + m_tokenRefreshTimer->start((expireTime - 20) * 1000); + } + }); +} + +void DevicePluginSonos::onHouseholdIdsReceived(QList householdIds) +{ + qDebug(dcSonos()) << "Household Id received, start to discover groups"; + Sonos *sonos = static_cast(sender()); + foreach(QByteArray householdId, householdIds) { + sonos->getGroups(householdId); + } +} diff --git a/sonos/devicepluginsonos.h b/sonos/devicepluginsonos.h index db84374b..075375b0 100644 --- a/sonos/devicepluginsonos.h +++ b/sonos/devicepluginsonos.h @@ -29,6 +29,7 @@ #include #include +#include class DevicePluginSonos : public DevicePlugin @@ -52,11 +53,20 @@ public: private: PluginTimer *m_pluginTimer = nullptr; + QTimer *m_tokenRefreshTimer = nullptr; + QHash m_sonosConnections; + QList m_householdIds; + + QByteArray m_sonosConnectionAccessToken; + QByteArray m_sonosConnectionRefreshToken; private slots: void onPluginTimer(); void onConnectionChanged(); + void onRefreshTimeout(); + + void onHouseholdIdsReceived(QList householdIds); }; diff --git a/sonos/devicepluginsonos.json b/sonos/devicepluginsonos.json index a59bd854..af19f4d0 100644 --- a/sonos/devicepluginsonos.json +++ b/sonos/devicepluginsonos.json @@ -16,6 +16,8 @@ "interfaces": ["gateway"], "createMethods": ["user"], "setupMethod": "oauth", + "paramTypes": [ + ], "stateTypes": [ { "id": "5aa4360c-61de-47d0-a72e-a19d57712e1c", diff --git a/sonos/sonos.cpp b/sonos/sonos.cpp index 20cf52dc..3d606860 100644 --- a/sonos/sonos.cpp +++ b/sonos/sonos.cpp @@ -41,9 +41,11 @@ void Sonos::getHouseholds() { QNetworkRequest request; request.setHeader(QNetworkRequest::KnownHeaders::ContentTypeHeader, "application/json"); - request.setRawHeader("Authorization", "Bearer" + m_accessToken); + request.setRawHeader("Authorization", "Bearer " + m_accessToken); + request.setRawHeader("X-Sonos-Api-Key", m_apiKey); 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(); @@ -58,8 +60,6 @@ void Sonos::getHouseholds() /*QJsonDocument data = reply->readAll(); if (!data.isObject()) return; - - QList households; emit householdObjectsReceived(households);*/ }); @@ -85,14 +85,38 @@ void Sonos::loadFavorite() } -void Sonos::getGroups() +void Sonos::getGroups(const QByteArray &householdId) { + QNetworkRequest request; + request.setHeader(QNetworkRequest::KnownHeaders::ContentTypeHeader, "application/json"); + request.setRawHeader("Authorization", "Bearer " + m_accessToken); + request.setRawHeader("X-Sonos-Api-Key", m_apiKey); + request.setUrl(QUrl(m_baseControlUrl + "/households/" + householdId + "/groups")); + QNetworkReply *reply = m_networkManager->get(request); + 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) { + qCWarning(dcSonos()) << "Request error:" << status << reply->errorString(); + return; + } + + qDebug(dcSonos()) << "Received response from Sonos" << reply->readAll(); + /*QJsonDocument data = reply->readAll(); + if (!data.isObject()) + return; + + QList households; + emit householdObjectsReceived(households);*/ + }); } -void Sonos::createGroup() +void Sonos::createGroup(const QByteArray &householdId, QList playerIds) { - + Q_UNUSED(householdId) + Q_UNUSED(playerIds) } void Sonos::modifyGroupMembers() @@ -100,105 +124,126 @@ void Sonos::modifyGroupMembers() } -void Sonos::setGroupMembers() -{ - -} - -void Sonos::getGroupVolume(int groupId) +void Sonos::setGroupMembers(const QByteArray &groupId) { Q_UNUSED(groupId) } -void Sonos::setGroupVolume(int groupId, int volume) +void Sonos::getGroupVolume(const QByteArray &groupId) { - Q_UNUSED(groupId) - Q_UNUSED(volume) + Q_UNUSED(groupId) } -void Sonos::setGroupMute(int groupId, bool mute) +void Sonos::setGroupVolume(const QByteArray &groupId, int volume) { - Q_UNUSED(groupId) - Q_UNUSED(mute) + Q_UNUSED(groupId) + Q_UNUSED(volume) } -void Sonos::setGroupRelativeVolume(int groupId, int volumeDelta) +void Sonos::setGroupMute(const QByteArray &groupId, bool mute) { - Q_UNUSED(groupId) - Q_UNUSED(volumeDelta) + Q_UNUSED(groupId) + Q_UNUSED(mute) } -void Sonos::getPlaybackStatus() +void Sonos::setGroupRelativeVolume(const QByteArray &groupId, int volumeDelta) { + Q_UNUSED(groupId) + Q_UNUSED(volumeDelta) +} + +void Sonos::getGroupPlaybackStatus(const QByteArray &groupId) +{ + Q_UNUSED(groupId) +} + +void Sonos::groupLoadLineIn(const QByteArray &groupId) +{ + Q_UNUSED(groupId) } -void Sonos::loadLineIn() +void Sonos::groupPlay(const QByteArray &groupId) { - + Q_UNUSED(groupId) } -void Sonos::play() +void Sonos::groupPause(const QByteArray &groupId) { - + Q_UNUSED(groupId) } -void Sonos::pause() +void Sonos::groupSeek(const QByteArray &groupId) { - + Q_UNUSED(groupId) } -void Sonos::seek() +void Sonos::groupSeekRelative(const QByteArray &groupId, int millis) { - + Q_UNUSED(groupId) + Q_UNUSED(millis) } -void Sonos::seekRelative() +void Sonos::groupSetPlayModes(const QByteArray &groupId, PlayMode playMode) { - + Q_UNUSED(groupId) + Q_UNUSED(playMode) } -void Sonos::setPlayModes() +void Sonos::groupSetShuffle(const QByteArray &groupId, bool shuffle) { - + Q_UNUSED(groupId) + Q_UNUSED(shuffle) } -void Sonos::skipToNextTrack() +void Sonos::groupSetRepeat(const QByteArray &groupId, RepeatMode repeatMode) { - + Q_UNUSED(groupId) + Q_UNUSED(repeatMode) } -void Sonos::skipToPreviousTrack() +void Sonos::groupSetCrossfade(const QByteArray &groupId, bool crossfade) { - + Q_UNUSED(groupId) + Q_UNUSED(crossfade) } -void Sonos::togglePlayPause() +void Sonos::groupSkipToNextTrack(const QByteArray &groupId) { - + Q_UNUSED(groupId) } -void Sonos::getPlayerVolume(int playerId) +void Sonos::groupSkipToPreviousTrack(const QByteArray &groupId) { - Q_UNUSED(playerId) + Q_UNUSED(groupId) } -void Sonos::setPlayerVolume(int playerId, int volume) +void Sonos::groupTogglePlayPause(const QByteArray &groupId) { - Q_UNUSED(playerId) - Q_UNUSED(volume) + Q_UNUSED(groupId) } -void Sonos::setPlayerRelativeVolume(int playerId, int volumeDelta) +void Sonos::getPlayerVolume(const QByteArray &playerId) { - Q_UNUSED(playerId) - Q_UNUSED(volumeDelta) + Q_UNUSED(playerId) } -void Sonos::setPlayerMute(int playerId, bool mute) +void Sonos::setPlayerVolume(const QByteArray &playerId, int volume) { - Q_UNUSED(playerId) - Q_UNUSED(mute) + Q_UNUSED(playerId) + Q_UNUSED(volume) +} + +void Sonos::setPlayerRelativeVolume(const QByteArray &playerId, int volumeDelta) +{ + Q_UNUSED(playerId) + Q_UNUSED(volumeDelta) +} + +void Sonos::setPlayerMute(const QByteArray &playerId, bool mute) +{ + Q_UNUSED(playerId) + Q_UNUSED(mute) } void Sonos::getPlaylist() diff --git a/sonos/sonos.h b/sonos/sonos.h index 4d98478a..d91c1191 100644 --- a/sonos/sonos.h +++ b/sonos/sonos.h @@ -33,19 +33,38 @@ class Sonos : public QObject Q_OBJECT public: - enum PlayMode { - Repeat, - RepeatOne, - Shuffle, - Crossfade + enum RepeatMode { + RepeatModeOne, + RepeatModeAll, + RepeatModeNone }; + struct PlayMode { + bool repeat; + bool repeatOne; + bool shuffle; + bool crossfade; + }; + + struct PlayerObject { + + }; + + /* Represents a Sonos household.*/ + struct GroupObject { + QByteArray CoordinatorId; //Player acting as the group coordinator for the group + QByteArray 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”. + }; + /* * Represents a Sonos household.*/ - struct HouseholdObject { + /*struct HouseholdObject { QString id; //Identifies a Sonos household. QString name; //A user-displayable name of the Sonos household - }; + };*/ /* * The music service identifier or a pseudo-service identifier in the case of local library. */ @@ -160,36 +179,40 @@ public: void getFavorites(); void loadFavorite(); - void getGroups(); - void createGroup(); + void getGroups(const QByteArray &householdId); + void createGroup(const QByteArray &householdId, QList playerIds); void modifyGroupMembers(); - void setGroupMembers(); + void setGroupMembers(const QByteArray &groupId); //group volume - void getGroupVolume(int groupId); //Get the volume and mute state of a group. - void setGroupVolume(int groupId, int volume); //Set group volume to a specific level and unmute the group if muted. - void setGroupMute(int groupId, bool mute); //Mute and unmute the group. - void setGroupRelativeVolume(int groupId, int volumeDelta); //Increase or decrease group volume. + void getGroupVolume(const QByteArray &groupId); //Get the volume and mute state of a group. + void setGroupVolume(const QByteArray &groupId, int volume); //Set group volume to a specific level and unmute the group if muted. + void setGroupMute(const QByteArray &groupId, bool mute); //Mute and unmute the group. + void setGroupRelativeVolume(const QByteArray &groupId, int volumeDelta); //Increase or decrease group volume. - //playback - void getPlaybackStatus(); - void loadLineIn(); - void play(); - void pause(); - void seek(); - void seekRelative(); - void setPlayModes(); - void skipToNextTrack(); - void skipToPreviousTrack(); - void togglePlayPause(); + //group playback + void getGroupPlaybackStatus(const QByteArray &groupId); + void groupLoadLineIn(const QByteArray &groupId); + void groupPlay(const QByteArray &groupId); + void groupPause(const QByteArray &groupId); + void groupSeek(const QByteArray &groupId); + void groupSeekRelative(const QByteArray &groupId, int deltaMillis); + void groupSetPlayModes(const QByteArray &groupId, PlayMode playMode); + void groupSetShuffle(const QByteArray &groupId, bool shuffle); + void groupSetRepeat(const QByteArray &groupId, RepeatMode repeatMode); + void groupSetCrossfade(const QByteArray &groupId, bool crossfade); + + void groupSkipToNextTrack(const QByteArray &groupId); + void groupSkipToPreviousTrack(const QByteArray &groupId); + void groupTogglePlayPause(const QByteArray &groupId); //playbackMetadata // playerVolume - void getPlayerVolume(int playerId); - void setPlayerVolume(int playerId, int volume); - void setPlayerRelativeVolume(int playerId, int volumeDelta); - void setPlayerMute(int playerId, bool mute); + void getPlayerVolume(const QByteArray &playerId); + void setPlayerVolume(const QByteArray &playerId, int volume); + void setPlayerRelativeVolume(const QByteArray &playerId, int volumeDelta); + void setPlayerMute(const QByteArray &playerId, bool mute); //Playlists API namespace void getPlaylist(); @@ -201,16 +224,18 @@ public: void setPlayerSettings(); private: - QByteArray m_baseAuthorizationUrl = "api.sonos.com/login/v3/oauth"; - QByteArray m_baseControlUrl = "api.ws.sonos.com/control/api/v1"; + QByteArray m_baseAuthorizationUrl = "https://api.sonos.com/login/v3/oauth"; + QByteArray m_baseControlUrl = "https://api.ws.sonos.com/control/api/v1"; QByteArray m_accessToken; + QByteArray m_apiKey = "b15cbf8c-a39c-47aa-bd93-635a96e9696c"; NetworkAccessManager *m_networkManager = nullptr; - private slots: signals: + void householdIdsReceived(QList householdIds); + void groupsReceived(QList groups); }; From ac58d41ea6382b8c1b392b57acfb03aec6bd91e2 Mon Sep 17 00:00:00 2001 From: nymea Date: Tue, 3 Sep 2019 23:42:49 +0200 Subject: [PATCH 10/22] first working version --- sonos/devicepluginsonos.cpp | 179 +++++++-- sonos/devicepluginsonos.h | 11 +- sonos/sonos.cpp | 701 ++++++++++++++++++++++++++++++++---- sonos/sonos.h | 174 +++++---- 4 files changed, 907 insertions(+), 158 deletions(-) diff --git a/sonos/devicepluginsonos.cpp b/sonos/devicepluginsonos.cpp index bba6e3a3..f071275d 100644 --- a/sonos/devicepluginsonos.cpp +++ b/sonos/devicepluginsonos.cpp @@ -45,7 +45,8 @@ DevicePluginSonos::~DevicePluginSonos() Device::DeviceSetupStatus DevicePluginSonos::setupDevice(Device *device) { if (!m_pluginTimer) { - hardwareManager()->pluginTimerManager()->registerTimer(1); + hardwareManager()->pluginTimerManager()->registerTimer(10); + connect(m_pluginTimer, &PluginTimer::timeout, this, &DevicePluginSonos::onPluginTimer); } if(!m_tokenRefreshTimer) { @@ -62,19 +63,25 @@ Device::DeviceSetupStatus DevicePluginSonos::setupDevice(Device *device) pluginStorage()->endGroup(); Sonos *sonos = new Sonos(hardwareManager()->networkManager(), accessToken, 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); m_sonosConnections.insert(device, sonos); } if (device->deviceClassId() == sonosGroupDeviceClassId) { - //set parent ID + } return Device::DeviceSetupStatusSuccess; } DevicePairingInfo DevicePluginSonos::pairDevice(DevicePairingInfo &devicePairingInfo) { - if (devicePairingInfo.deviceClassId() == sonosConnectionDeviceClassId) { QString clientId = "b15cbf8c-a39c-47aa-bd93-635a96e9696c"; QString clientSecret = "c086ba71-e562-430b-a52f-867c6482fd11"; @@ -153,11 +160,12 @@ DevicePairingInfo DevicePluginSonos::confirmPairing(DevicePairingInfo &devicePai pluginStorage()->setValue("refresh_token", refreshToken); pluginStorage()->endGroup(); - /*if (jsonDoc.toVariant().toMap().contains("expires_in")) { + 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(); - m_tokenRefreshTimer->start((expireTime - 20) * 1000); - }*/ + //m_tokenRefreshTimer->start((expireTime - 20) * 1000); + //TODO + } info.setStatus(Device::DeviceErrorNoError); emit pairingFinished(info); @@ -179,7 +187,15 @@ void DevicePluginSonos::postSetupDevice(Device *device) } 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); } } @@ -188,7 +204,7 @@ void DevicePluginSonos::startMonitoringAutoDevices() { foreach (Device *device, myDevices()) { if (device->deviceClassId() == sonosGroupDeviceClassId) { - return; // We already have a Auto Mock device... do nothing. + return; } } } @@ -197,13 +213,14 @@ void DevicePluginSonos::deviceRemoved(Device *device) { qCDebug(dcSonos) << "Delete " << device->name(); if (myDevices().empty()) { + hardwareManager()->pluginTimerManager()->unregisterTimer(m_pluginTimer); + m_pluginTimer = nullptr; } } Device::DeviceError DevicePluginSonos::executeAction(Device *device, const Action &action) { - Q_UNUSED(action) if (device->deviceClassId() == sonosGroupDeviceClassId) { Sonos *sonos = m_sonosConnections.value(device); QByteArray groupId = device->paramValue(sonosGroupDeviceGroupIdParamTypeId).toByteArray(); @@ -237,29 +254,41 @@ Device::DeviceError DevicePluginSonos::executeAction(Device *device, const Actio } if (action.actionTypeId() == sonosGroupPauseActionTypeId) { - sonos->groupPause(groupId); - return Device::DeviceErrorNoError; + m_pendingActions.insert(sonos->groupPause(groupId), action.id()); + return Device::DeviceErrorAsync; } if (action.actionTypeId() == sonosGroupStopActionTypeId) { - sonos->groupPause(groupId); - return Device::DeviceErrorNoError; + m_pendingActions.insert(sonos->groupPause(groupId), action.id()); + return Device::DeviceErrorAsync; } if (action.actionTypeId() == sonosGroupMuteActionTypeId) { bool mute = action.param(sonosGroupMuteActionMuteParamTypeId).value().toBool(); - sonos->setGroupMute(groupId, mute); - return Device::DeviceErrorNoError; + m_pendingActions.insert(sonos->setGroupMute(groupId, mute), action.id()); + return Device::DeviceErrorAsync; } if (action.actionTypeId() == sonosGroupSkipNextActionTypeId) { - sonos->groupSkipToNextTrack(groupId); - return Device::DeviceErrorNoError; + m_pendingActions.insert(sonos->groupSkipToNextTrack(groupId), action.id()); + return Device::DeviceErrorAsync; } if (action.actionTypeId() == sonosGroupSkipBackActionTypeId) { - sonos->groupSkipToPreviousTrack(groupId); - return Device::DeviceErrorNoError; + m_pendingActions.insert(sonos->groupSkipToPreviousTrack(groupId), action.id()); + return Device::DeviceErrorAsync; + } + + if (action.actionTypeId() == sonosGroupPlaybackStatusActionTypeId) { + QString playbackStatus = action.param(sonosGroupPlaybackStatusActionPlaybackStatusParamTypeId).value().toString(); + if (playbackStatus == "Playing") { + sonos->groupPlay(groupId); + } else if(playbackStatus == "Stopped") { + sonos->groupPause(groupId); + } else if(playbackStatus == "Paused") { + sonos->groupPause(groupId); + } + return Device::DeviceErrorAsync; } return Device::DeviceErrorActionTypeNotFound; } @@ -267,16 +296,36 @@ Device::DeviceError DevicePluginSonos::executeAction(Device *device, const Actio } void DevicePluginSonos::onPluginTimer() -{ +{ + foreach (Device *device, myDevices().filterByDeviceClassId(sonosConnectionDeviceClassId)) { + + //get groups for each household in order to add or remove groups + Sonos *sonos = m_sonosConnections.value(device); + if (!sonos) + continue; + sonos->getHouseholds(); + + foreach (Device *groupDevice, myDevices().filterByParentDeviceId(device->id())) { + if (device->deviceClassId() == sonosGroupDeviceClassId) { + //get playback status of each group + QByteArray groupId = groupDevice->paramValue(sonosGroupDeviceGroupIdParamTypeId).toByteArray(); + sonos->getGroupPlaybackStatus(groupId); + sonos->getGroupMetadataStatus(groupId); + sonos->getGroupVolume(groupId); + } + } + } } -void DevicePluginSonos::onConnectionChanged() +void DevicePluginSonos::onConnectionChanged(bool connected) { Sonos *sonos = static_cast(sender()); Device *device = m_sonosConnections.key(sonos); - device->setStateValue(sonosConnectionConnectedStateTypeId, false); //TODO + device->setStateValue(sonosConnectionConnectedStateTypeId, connected); - //TODO set all groups + foreach (Device *groupDevice, myDevices().filterByParentDeviceId(device->id())) { + groupDevice->setStateValue(sonosGroupConnectedStateTypeId, connected); + } } void DevicePluginSonos::onRefreshTimeout() @@ -310,11 +359,89 @@ void DevicePluginSonos::onRefreshTimeout() }); } -void DevicePluginSonos::onHouseholdIdsReceived(QList householdIds) +void DevicePluginSonos::onHouseholdIdsReceived(QList householdIds) { - qDebug(dcSonos()) << "Household Id received, start to discover groups"; Sonos *sonos = static_cast(sender()); - foreach(QByteArray householdId, householdIds) { + foreach(QString householdId, householdIds) { sonos->getGroups(householdId); } } + +void DevicePluginSonos::onGroupsReceived(QList groupObjects) +{ + Sonos *sonos = static_cast(sender()); + Device *parentDevice = m_sonosConnections.key(sonos); + if (!parentDevice) + return; + + QList deviceDescriptors; + foreach(Sonos::GroupObject groupObject, groupObjects) { + if (!myDevices().filterByParam(sonosGroupDeviceGroupIdParamTypeId, groupObject.groupId).isEmpty()) { + continue; + } + DeviceDescriptor deviceDescriptor(sonosGroupDeviceClassId, groupObject.displayName, "Sonos Group", parentDevice->id()); + ParamList params; + params.append(Param(sonosGroupDeviceGroupIdParamTypeId, groupObject.groupId)); + deviceDescriptor.setParams(params); + deviceDescriptors.append(deviceDescriptor); + } + + if (!deviceDescriptors.isEmpty()) + emit autoDevicesAppeared(sonosGroupDeviceClassId, deviceDescriptors); +} + +void DevicePluginSonos::onPlayBackStatusReceived(const QString &groupId, Sonos::PlayBackObject playBack) +{ + qDebug(dcSonos()) << "Playback status received" << playBack.playbackState; + Device *device = myDevices().findByParams(ParamList() << Param(sonosGroupDeviceGroupIdParamTypeId, groupId)); + if (!device) + return; + + 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); + //device->setStateValue(sonosGroupArtworkStateTypeId, metaDataStatus.currentItem.track.imageUrl); + device->setStateValue(sonosGroupArtworkStateTypeId, metaDataStatus.container.imageUrl); +} + +void DevicePluginSonos::onVolumeReceived(const QString &groupId, Sonos::GroupVolumeObject 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)) { + ActionId nymeaActionId = m_pendingActions.value(sonosActionId); + if (success) { + emit actionExecutionFinished(nymeaActionId, Device::DeviceErrorNoError); + } else { + emit actionExecutionFinished(nymeaActionId, Device::DeviceErrorHardwareFailure); + } + } +} diff --git a/sonos/devicepluginsonos.h b/sonos/devicepluginsonos.h index 075375b0..5820bd73 100644 --- a/sonos/devicepluginsonos.h +++ b/sonos/devicepluginsonos.h @@ -61,13 +61,18 @@ private: QByteArray m_sonosConnectionAccessToken; QByteArray m_sonosConnectionRefreshToken; + QHash m_pendingActions; private slots: void onPluginTimer(); - void onConnectionChanged(); + void onConnectionChanged(bool connected); void onRefreshTimeout(); - void onHouseholdIdsReceived(QList householdIds); - + void onHouseholdIdsReceived(QList householdIds); + void onGroupsReceived(QList groupIds); + void onPlayBackStatusReceived(const QString &groupId, Sonos::PlayBackObject playBack); + void onMetadataStatusReceived(const QString &groupId, Sonos::MetadataStatus metaDataStatus); + void onVolumeReceived(const QString &groupId, Sonos::GroupVolumeObject groupVolume); + void onActionExecuted(QUuid actionId, bool success); }; #endif // DEVICEPLUGINSONOS_H diff --git a/sonos/sonos.cpp b/sonos/sonos.cpp index 3d606860..7aa13fe6 100644 --- a/sonos/sonos.cpp +++ b/sonos/sonos.cpp @@ -24,6 +24,8 @@ #include "extern-plugininfo.h" #include +#include +#include Sonos::Sonos(NetworkAccessManager *networkmanager, const QByteArray &accessToken, QObject *parent) : QObject(parent), @@ -50,29 +52,40 @@ void Sonos::getHouseholds() reply->deleteLater(); int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); + connectionChanged(true); // Check HTTP status code if (status != 200 || reply->error() != QNetworkReply::NoError) { qCWarning(dcSonos()) << "Request error:" << status << reply->errorString(); return; } - qDebug(dcSonos()) << "Received response from Sonos" << reply->readAll(); - /*QJsonDocument data = reply->readAll(); - if (!data.isObject()) + //qDebug(dcSonos()) << "Received response from Sonos" << reply->readAll(); + QJsonDocument data = QJsonDocument::fromJson(reply->readAll()); + if (!data.isObject()) { + qDebug(dcSonos()) << "Household ID: Recieved invalide JSON object"; return; - QList households; - emit householdObjectsReceived(households);*/ + } + QList households; + QJsonArray jsonArray = data["households"].toArray(); + foreach (const QJsonValue & value, jsonArray) { + QJsonObject obj = value.toObject(); + qDebug(dcSonos()) << "Household ID received:" << obj["id"].toString(); + households.append(obj["id"].toString()); + } + emit householdIdsReceived(households); }); } -void Sonos::cancelAudioClip() +QUuid Sonos::cancelAudioClip() { - + QUuid actionId = QUuid::createUuid(); + return actionId; } -void Sonos::loadAudioClip() +QUuid Sonos::loadAudioClip() { - + QUuid actionId = QUuid::createUuid(); + return actionId; } void Sonos::getFavorites() @@ -80,12 +93,13 @@ void Sonos::getFavorites() } -void Sonos::loadFavorite() +QUuid Sonos::loadFavorite() { - + QUuid actionId = QUuid::createUuid(); + return actionId; } -void Sonos::getGroups(const QByteArray &householdId) +void Sonos::getGroups(const QString &householdId) { QNetworkRequest request; request.setHeader(QNetworkRequest::KnownHeaders::ContentTypeHeader, "application/json"); @@ -103,124 +117,673 @@ void Sonos::getGroups(const QByteArray &householdId) return; } - qDebug(dcSonos()) << "Received response from Sonos" << reply->readAll(); - /*QJsonDocument data = reply->readAll(); + //qDebug(dcSonos()) << "Received response from Sonos" << reply->readAll(); + QJsonDocument data = QJsonDocument::fromJson(reply->readAll()); if (!data.isObject()) return; - QList households; - emit householdObjectsReceived(households);*/ + if (!data["groups"].isArray()) + return; + + QJsonArray array = data["groups"].toArray(); + QList groupObjects; + foreach (const QJsonValue & value, array) { + QJsonObject obj = value.toObject(); + qDebug(dcSonos()) << "Group ID received:" << obj["id"].toString(); + GroupObject group; + group.groupId = obj["id"].toString(); + group.displayName = obj["name"].toString(); + groupObjects.append(group); + } + emit groupsReceived(groupObjects); }); } -void Sonos::createGroup(const QByteArray &householdId, QList playerIds) +QUuid Sonos::createGroup(const QString &householdId, QList playerIds) { Q_UNUSED(householdId) Q_UNUSED(playerIds) + QUuid actionId = QUuid::createUuid(); + return actionId; } -void Sonos::modifyGroupMembers() +QUuid Sonos::modifyGroupMembers() { - + QUuid actionId = QUuid::createUuid(); + return actionId; } -void Sonos::setGroupMembers(const QByteArray &groupId) +QUuid Sonos::setGroupMembers(const QString &groupId) { Q_UNUSED(groupId) + QUuid actionId = QUuid::createUuid(); + return actionId; } -void Sonos::getGroupVolume(const QByteArray &groupId) +void Sonos::getGroupVolume(const QString &groupId) { - 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_apiKey); + 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) { + qCWarning(dcSonos()) << "Request error:" << status << reply->errorString(); + return; + } + + //qDebug(dcSonos()) << "Received response from Sonos" << reply->readAll(); + QJsonDocument data = QJsonDocument::fromJson(reply->readAll()); + if (!data.isObject()) + return; + + GroupVolumeObject groupVolume; + + groupVolume.volume = data["volume"].toInt(); + groupVolume.muted = data["muted"].toBool(); + groupVolume.fixed = data["fixed"].toBool(); + + emit volumeReceived(groupId, groupVolume); + }); } -void Sonos::setGroupVolume(const QByteArray &groupId, int volume) +QUuid Sonos::setGroupVolume(const QString &groupId, int volume) { - Q_UNUSED(groupId) - Q_UNUSED(volume) + QNetworkRequest request; + request.setHeader(QNetworkRequest::KnownHeaders::ContentTypeHeader, "application/json"); + request.setRawHeader("Authorization", "Bearer " + m_accessToken); + request.setRawHeader("X-Sonos-Api-Key", m_apiKey); + request.setUrl(QUrl(m_baseControlUrl + "/groups/" + groupId + "/groupVolume")); + QUuid actionId = QUuid::createUuid(); + + QJsonObject object; + object.insert("volume", QJsonValue::fromVariant(volume)); + QJsonDocument doc(object); + + QNetworkReply *reply = m_networkManager->post(request, doc.toBinaryData()); + 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) { + qCWarning(dcSonos()) << "Request error:" << status << reply->errorString(); + emit actionExecuted(actionId, false); + return; + } + emit actionExecuted(actionId, true); + }); + return actionId; } -void Sonos::setGroupMute(const QByteArray &groupId, bool mute) +QUuid Sonos::setGroupMute(const QString &groupId, bool mute) { Q_UNUSED(groupId) Q_UNUSED(mute) + + QNetworkRequest request; + request.setHeader(QNetworkRequest::KnownHeaders::ContentTypeHeader, "application/json"); + request.setRawHeader("Authorization", "Bearer " + m_accessToken); + request.setRawHeader("X-Sonos-Api-Key", m_apiKey); + request.setUrl(QUrl(m_baseControlUrl + "/groups/" + groupId + "/groupVolume/mute")); + QUuid actionId = QUuid::createUuid(); + + QJsonObject object; + object.insert("muted", QJsonValue::fromVariant(mute)); + QJsonDocument doc(object); + + QNetworkReply *reply = m_networkManager->post(request, doc.toBinaryData()); + 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) { + qCWarning(dcSonos()) << "Request error:" << status << reply->errorString(); + emit actionExecuted(actionId, false); + return; + } + emit actionExecuted(actionId, true); + }); + return actionId; } -void Sonos::setGroupRelativeVolume(const QByteArray &groupId, int volumeDelta) +QUuid Sonos::setGroupRelativeVolume(const QString &groupId, int volumeDelta) { - Q_UNUSED(groupId) - Q_UNUSED(volumeDelta) + QNetworkRequest request; + request.setHeader(QNetworkRequest::KnownHeaders::ContentTypeHeader, "application/json"); + request.setRawHeader("Authorization", "Bearer " + m_accessToken); + request.setRawHeader("X-Sonos-Api-Key", m_apiKey); + request.setUrl(QUrl(m_baseControlUrl + "/groups/" + groupId + "/groupVolume/relative")); + QUuid actionId = QUuid::createUuid(); + + QJsonObject object; + object.insert("volumeDelta", QJsonValue::fromVariant(volumeDelta)); + QJsonDocument doc(object); + + QNetworkReply *reply = m_networkManager->post(request, doc.toBinaryData()); + 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) { + qCWarning(dcSonos()) << "Request error:" << status << reply->errorString(); + emit actionExecuted(actionId, false); + return; + } + emit actionExecuted(actionId, true); + }); + return actionId; } -void Sonos::getGroupPlaybackStatus(const QByteArray &groupId) +void Sonos::getGroupPlaybackStatus(const QString &groupId) { - 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_apiKey); + 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) { + qCWarning(dcSonos()) << "Request error:" << status << reply->errorString(); + return; + } + + QJsonDocument data = QJsonDocument::fromJson(reply->readAll()); + if (!data.isObject()) + return; + + PlayBackObject playBackObject; + QJsonObject object = data.object(); + playBackObject.itemId = object["itemId"].toString(); + playBackObject.positionMillis = object["positionMillis"].toInt(); + playBackObject.previousItemId = object["previousItemId"].toInt(); + playBackObject.previousPositionMillis = object["previousPositionMillis"].toInt(); + QString playBackState = object["playbackState"].toString(); + if (playBackState.contains("BUFFERING")) { + playBackObject.playbackState = PlayBackStateBuffering; + } else if (playBackState.contains("IDLE")) { + playBackObject.playbackState = PlayBackStateIdle; + } else if (playBackState.contains("PAUSE")) { + playBackObject.playbackState = PlayBackStatePause; + } else if (playBackState.contains("PLAYING")) { + playBackObject.playbackState = PlayBackStatePlaying; + } + playBackObject.isDucking = object["isDucking"].toBool(); + playBackObject.queueVersion = object["queueVersion"].toString(); + emit playBackStatusReceived(groupId, playBackObject); + }); } -void Sonos::groupLoadLineIn(const QByteArray &groupId) +QUuid Sonos::groupLoadLineIn(const QString &groupId) { Q_UNUSED(groupId) - + QUuid actionId = QUuid::createUuid(); + return actionId; } -void Sonos::groupPlay(const QByteArray &groupId) +QUuid Sonos::groupPlay(const QString &groupId) { - 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_apiKey); + request.setUrl(QUrl(m_baseControlUrl + "/groups/" + groupId + "/playback/play")); + QUuid actionId = QUuid::createUuid(); + + QNetworkReply *reply = m_networkManager->post(request, ""); + 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) { + qCWarning(dcSonos()) << "Request error:" << status << reply->errorString(); + emit actionExecuted(actionId, false); + return; + } + emit actionExecuted(actionId, true); + }); + return actionId; } -void Sonos::groupPause(const QByteArray &groupId) +QUuid Sonos::groupPause(const QString &groupId) { - 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_apiKey); + request.setUrl(QUrl(m_baseControlUrl + "/groups/" + groupId + "/playback/pause")); + QUuid actionId = QUuid::createUuid(); + + QNetworkReply *reply = m_networkManager->post(request, ""); + 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) { + qCWarning(dcSonos()) << "Request error:" << status << reply->errorString(); + emit actionExecuted(actionId, false); + return; + } + emit actionExecuted(actionId, true); + }); + return actionId; } -void Sonos::groupSeek(const QByteArray &groupId) +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_apiKey); + 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.toBinaryData()); + 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) { + qCWarning(dcSonos()) << "Request error:" << status << reply->errorString(); + emit actionExecuted(actionId, false); + return; + } + emit actionExecuted(actionId, true); + }); + return actionId; } -void Sonos::groupSeekRelative(const QByteArray &groupId, int millis) +QUuid Sonos::groupSeekRelative(const QString &groupId, int deltaMillis) { Q_UNUSED(groupId) - Q_UNUSED(millis) + QNetworkRequest request; + request.setHeader(QNetworkRequest::KnownHeaders::ContentTypeHeader, "application/json"); + request.setRawHeader("Authorization", "Bearer " + m_accessToken); + request.setRawHeader("X-Sonos-Api-Key", m_apiKey); + 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.toBinaryData()); + 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) { + qCWarning(dcSonos()) << "Request error:" << status << reply->errorString(); + emit actionExecuted(actionId, false); + return; + } + emit actionExecuted(actionId, true); + }); + return actionId; } -void Sonos::groupSetPlayModes(const QByteArray &groupId, PlayMode playMode) +QUuid Sonos::groupSetPlayModes(const QString &groupId, PlayMode playMode) { - Q_UNUSED(groupId) - Q_UNUSED(playMode) + QNetworkRequest request; + request.setHeader(QNetworkRequest::KnownHeaders::ContentTypeHeader, "application/json"); + request.setRawHeader("Authorization", "Bearer " + m_accessToken); + request.setRawHeader("X-Sonos-Api-Key", m_apiKey); + 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.toBinaryData()); + 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) { + qCWarning(dcSonos()) << "Request error:" << status << reply->errorString(); + emit actionExecuted(actionId, false); + return; + } + emit actionExecuted(actionId, true); + }); + return actionId; } -void Sonos::groupSetShuffle(const QByteArray &groupId, bool shuffle) +QUuid Sonos::groupSetShuffle(const QString &groupId, bool shuffle) { - Q_UNUSED(groupId) - Q_UNUSED(shuffle) + QNetworkRequest request; + request.setHeader(QNetworkRequest::KnownHeaders::ContentTypeHeader, "application/json"); + request.setRawHeader("Authorization", "Bearer " + m_accessToken); + request.setRawHeader("X-Sonos-Api-Key", m_apiKey); + 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.toBinaryData()); + 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) { + qCWarning(dcSonos()) << "Request error:" << status << reply->errorString(); + emit actionExecuted(actionId, false); + return; + } + emit actionExecuted(actionId, true); + }); + return actionId; } -void Sonos::groupSetRepeat(const QByteArray &groupId, RepeatMode repeatMode) +QUuid Sonos::groupSetRepeat(const QString &groupId, RepeatMode repeatMode) { - Q_UNUSED(groupId) - Q_UNUSED(repeatMode) + QNetworkRequest request; + request.setHeader(QNetworkRequest::KnownHeaders::ContentTypeHeader, "application/json"); + request.setRawHeader("Authorization", "Bearer " + m_accessToken); + request.setRawHeader("X-Sonos-Api-Key", m_apiKey); + request.setUrl(QUrl(m_baseControlUrl + "/groups/" + groupId + "/playback/playMode")); + QUuid actionId = QUuid::createUuid(); + + QJsonObject object; + QJsonObject playModesObject; + if (repeatMode == RepeatModeAll) { + playModesObject["repeat"] = true; + playModesObject["repeatOne"] = false; + } else if (repeatMode == RepeatModeOne) { + playModesObject["repeat"] = false; + playModesObject["repeatOne"] = true; + } else if (repeatMode == RepeatModeAll) { + playModesObject["repeat"] = false; + playModesObject["repeatOne"] = false; + } + object.insert("playModes", playModesObject); + QJsonDocument doc(object); + + QNetworkReply *reply = m_networkManager->post(request, doc.toBinaryData()); + 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) { + qCWarning(dcSonos()) << "Request error:" << status << reply->errorString(); + emit actionExecuted(actionId, false); + return; + } + emit actionExecuted(actionId, true); + }); + return actionId; } -void Sonos::groupSetCrossfade(const QByteArray &groupId, bool crossfade) +QUuid Sonos::groupSetCrossfade(const QString &groupId, bool crossfade) { - Q_UNUSED(groupId) - Q_UNUSED(crossfade) + QNetworkRequest request; + request.setHeader(QNetworkRequest::KnownHeaders::ContentTypeHeader, "application/json"); + request.setRawHeader("Authorization", "Bearer " + m_accessToken); + request.setRawHeader("X-Sonos-Api-Key", m_apiKey); + 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.toBinaryData()); + 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) { + qCWarning(dcSonos()) << "Request error:" << status << reply->errorString(); + emit actionExecuted(actionId, false); + return; + } + emit actionExecuted(actionId, true); + }); + return actionId; } -void Sonos::groupSkipToNextTrack(const QByteArray &groupId) +QUuid Sonos::groupSkipToNextTrack(const QString &groupId) { - 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_apiKey); + 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, this] { + reply->deleteLater(); + int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); + + // Check HTTP status code + if (status != 200 || reply->error() != QNetworkReply::NoError) { + qCWarning(dcSonos()) << "Request error:" << status << reply->errorString(); + actionExecuted(actionId, false); + return; + } + actionExecuted(actionId, true); + }); + return actionId; } -void Sonos::groupSkipToPreviousTrack(const QByteArray &groupId) +QUuid Sonos::groupSkipToPreviousTrack(const QString &groupId) { - 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_apiKey); + 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, this] { + reply->deleteLater(); + int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); + + // Check HTTP status code + if (status != 200 || reply->error() != QNetworkReply::NoError) { + qCWarning(dcSonos()) << "Request error:" << status << reply->errorString(); + actionExecuted(actionId, false); + return; + } + actionExecuted(actionId, true); + }); + return actionId; } -void Sonos::groupTogglePlayPause(const QByteArray &groupId) +QUuid Sonos::groupTogglePlayPause(const QString &groupId) { - 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_apiKey); + 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, this] { + reply->deleteLater(); + int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); + + // Check HTTP status code + if (status != 200 || reply->error() != QNetworkReply::NoError) { + qCWarning(dcSonos()) << "Request error:" << status << reply->errorString(); + actionExecuted(actionId, false); + return; + } + actionExecuted(actionId, true); + }); + 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_apiKey); + 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) { + qCWarning(dcSonos()) << "Request error:" << status << reply->errorString(); + return; + } + + 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")) { + qDebug(dcSonos()) << "Item ID" << containerObject.value("id").toString(); + } + 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")) { + qDebug(dcSonos()) << "Item ID" << trackObject.value("id").toString(); + } + + 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")) { + qDebug(dcSonos()) << "Item ID" << trackObject.value("id").toString(); + } + 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) @@ -228,22 +791,28 @@ void Sonos::getPlayerVolume(const QByteArray &playerId) Q_UNUSED(playerId) } -void Sonos::setPlayerVolume(const QByteArray &playerId, int volume) +QUuid Sonos::setPlayerVolume(const QByteArray &playerId, int volume) { Q_UNUSED(playerId) Q_UNUSED(volume) + QUuid actionId = QUuid::createUuid(); + return actionId; } -void Sonos::setPlayerRelativeVolume(const QByteArray &playerId, int volumeDelta) +QUuid Sonos::setPlayerRelativeVolume(const QByteArray &playerId, int volumeDelta) { Q_UNUSED(playerId) Q_UNUSED(volumeDelta) + QUuid actionId = QUuid::createUuid(); + return actionId; } -void Sonos::setPlayerMute(const QByteArray &playerId, bool mute) +QUuid Sonos::setPlayerMute(const QByteArray &playerId, bool mute) { Q_UNUSED(playerId) Q_UNUSED(mute) + QUuid actionId = QUuid::createUuid(); + return actionId; } void Sonos::getPlaylist() @@ -256,9 +825,10 @@ void Sonos::getPlaylists() } -void Sonos::loadPlaylist() +QUuid Sonos::loadPlaylist() { - + QUuid actionId = QUuid::createUuid(); + return actionId; } void Sonos::getPlayerSettings() @@ -266,8 +836,9 @@ void Sonos::getPlayerSettings() } -void Sonos::setPlayerSettings() +QUuid Sonos::setPlayerSettings() { - + QUuid actionId = QUuid::createUuid(); + return actionId; } diff --git a/sonos/sonos.h b/sonos/sonos.h index d91c1191..541e345e 100644 --- a/sonos/sonos.h +++ b/sonos/sonos.h @@ -39,6 +39,13 @@ public: RepeatModeNone }; + enum PlayBackState { + PlayBackStateBuffering, + PlayBackStateIdle, + PlayBackStatePause, + PlayBackStatePlaying + }; + struct PlayMode { bool repeat; bool repeatOne; @@ -50,15 +57,20 @@ public: }; - /* Represents a Sonos household.*/ - struct GroupObject { - QByteArray CoordinatorId; //Player acting as the group coordinator for the group - QByteArray 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”. - }; + /* 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 GroupVolumeObject { + 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. + }; /* * Represents a Sonos household.*/ /*struct HouseholdObject { @@ -100,16 +112,7 @@ public: bool monoMode; bool wifiDisabled; }; - /* - * 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 - { - bool canCrossfade; - bool canSkip; - int durationMillis; - }; + /* The music object identifier for the item in a music service. * This identifies the content within a music service, the music service, @@ -130,17 +133,6 @@ public: bool isVisible; }; - /* 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; - }; - /* The artist of the track. */ struct ArtistObject { @@ -154,7 +146,6 @@ public: { QString name; ArtistObject artist; - //TODO }; struct ContainerObject @@ -167,76 +158,131 @@ public: //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; + }; + explicit Sonos(NetworkAccessManager *networkManager, const QByteArray &accessToken, QObject *parent = nullptr); void setAccessToken(const QByteArray &accessToken); void getHouseholds(); - - void cancelAudioClip(); - void loadAudioClip(); - void getFavorites(); - void loadFavorite(); + void getGroups(const QString &householdId); - void getGroups(const QByteArray &householdId); - void createGroup(const QByteArray &householdId, QList playerIds); - void modifyGroupMembers(); - void setGroupMembers(const QByteArray &groupId); + QUuid cancelAudioClip(); + QUuid loadAudioClip(); + QUuid loadFavorite(); - //group volume - void getGroupVolume(const QByteArray &groupId); //Get the volume and mute state of a group. - void setGroupVolume(const QByteArray &groupId, int volume); //Set group volume to a specific level and unmute the group if muted. - void setGroupMute(const QByteArray &groupId, bool mute); //Mute and unmute the group. - void setGroupRelativeVolume(const QByteArray &groupId, int volumeDelta); //Increase or decrease group volume. + QUuid createGroup(const QString &householdId, QList playerIds); + QUuid modifyGroupMembers(); + QUuid setGroupMembers(const QString &groupId); + + //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 QByteArray &groupId); - void groupLoadLineIn(const QByteArray &groupId); - void groupPlay(const QByteArray &groupId); - void groupPause(const QByteArray &groupId); - void groupSeek(const QByteArray &groupId); - void groupSeekRelative(const QByteArray &groupId, int deltaMillis); - void groupSetPlayModes(const QByteArray &groupId, PlayMode playMode); - void groupSetShuffle(const QByteArray &groupId, bool shuffle); - void groupSetRepeat(const QByteArray &groupId, RepeatMode repeatMode); - void groupSetCrossfade(const QByteArray &groupId, bool crossfade); + void getGroupPlaybackStatus(const QString &groupId); - void groupSkipToNextTrack(const QByteArray &groupId); - void groupSkipToPreviousTrack(const QByteArray &groupId); - void groupTogglePlayPause(const QByteArray &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); - void setPlayerVolume(const QByteArray &playerId, int volume); - void setPlayerRelativeVolume(const QByteArray &playerId, int volumeDelta); - void setPlayerMute(const QByteArray &playerId, bool mute); + 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 getPlaylist(); void getPlaylists(); - void loadPlaylist(); + QUuid loadPlaylist(); //Settings void getPlayerSettings(); - void setPlayerSettings(); + QUuid setPlayerSettings(); private: QByteArray m_baseAuthorizationUrl = "https://api.sonos.com/login/v3/oauth"; QByteArray m_baseControlUrl = "https://api.ws.sonos.com/control/api/v1"; - QByteArray m_accessToken; QByteArray m_apiKey = "b15cbf8c-a39c-47aa-bd93-635a96e9696c"; + QByteArray m_accessToken; NetworkAccessManager *m_networkManager = nullptr; private slots: signals: - void householdIdsReceived(QList householdIds); + void connectionChanged(bool connected); + void householdIdsReceived(QList householdIds); void groupsReceived(QList groups); + void playBackStatusReceived(const QString &groupId, PlayBackObject playBack); + void metadataStatusReceived(const QString &groupId, MetadataStatus metaDataStatus); + void volumeReceived(const QString &groupId, GroupVolumeObject groupVolume); + + void actionExecuted(QUuid actionId,bool success); + }; #endif // SONOS_H From 5ba83790c81223611411a688248ace1cd8478afe Mon Sep 17 00:00:00 2001 From: nymea Date: Wed, 4 Sep 2019 17:23:13 +0200 Subject: [PATCH 11/22] all actions are now functional --- sonos/devicepluginsonos.cpp | 185 +++++++++---- sonos/devicepluginsonos.h | 10 +- sonos/sonos.cpp | 521 +++++++++++++++++++++++++++++------- sonos/sonos.h | 63 ++--- 4 files changed, 595 insertions(+), 184 deletions(-) diff --git a/sonos/devicepluginsonos.cpp b/sonos/devicepluginsonos.cpp index f071275d..5441b669 100644 --- a/sonos/devicepluginsonos.cpp +++ b/sonos/devicepluginsonos.cpp @@ -38,15 +38,49 @@ DevicePluginSonos::DevicePluginSonos() DevicePluginSonos::~DevicePluginSonos() { - hardwareManager()->pluginTimerManager()->unregisterTimer(m_pluginTimer); + hardwareManager()->pluginTimerManager()->unregisterTimer(m_pluginTimer5sec); + hardwareManager()->pluginTimerManager()->unregisterTimer(m_pluginTimer60sec); } Device::DeviceSetupStatus DevicePluginSonos::setupDevice(Device *device) { - if (!m_pluginTimer) { - hardwareManager()->pluginTimerManager()->registerTimer(10); - connect(m_pluginTimer, &PluginTimer::timeout, this, &DevicePluginSonos::onPluginTimer); + 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(!m_tokenRefreshTimer) { @@ -74,8 +108,6 @@ Device::DeviceSetupStatus DevicePluginSonos::setupDevice(Device *device) } if (device->deviceClassId() == sonosGroupDeviceClassId) { - - } return Device::DeviceSetupStatusSuccess; } @@ -213,8 +245,10 @@ void DevicePluginSonos::deviceRemoved(Device *device) { qCDebug(dcSonos) << "Delete " << device->name(); if (myDevices().empty()) { - hardwareManager()->pluginTimerManager()->unregisterTimer(m_pluginTimer); - m_pluginTimer = nullptr; + hardwareManager()->pluginTimerManager()->unregisterTimer(m_pluginTimer5sec); + hardwareManager()->pluginTimerManager()->unregisterTimer(m_pluginTimer60sec); + m_pluginTimer5sec = nullptr; + m_pluginTimer60sec = nullptr; } } @@ -222,31 +256,32 @@ void DevicePluginSonos::deviceRemoved(Device *device) Device::DeviceError DevicePluginSonos::executeAction(Device *device, const Action &action) { if (device->deviceClassId() == sonosGroupDeviceClassId) { - Sonos *sonos = m_sonosConnections.value(device); - QByteArray groupId = device->paramValue(sonosGroupDeviceGroupIdParamTypeId).toByteArray(); + Sonos *sonos = m_sonosConnections.value(myDevices().findById(device->parentId())); + QString groupId = device->paramValue(sonosGroupDeviceGroupIdParamTypeId).toString(); - if (!sonos) + if (!sonos) { + qWarning(dcSonos()) << "Action cannot be executed: Sonos connection not available"; return Device::DeviceErrorInvalidParameter; + } if (action.actionTypeId() == sonosGroupPlayActionTypeId) { - sonos->groupPlay(groupId); + m_pendingActions.insert(sonos->groupPlay(groupId), action.id()); return Device::DeviceErrorAsync; } if (action.actionTypeId() == sonosGroupShuffleActionTypeId) { bool shuffle = action.param(sonosGroupShuffleActionShuffleParamTypeId).value().toBool(); - sonos->groupSetShuffle(groupId, shuffle); + m_pendingActions.insert(sonos->groupSetShuffle(groupId, shuffle), action.id()); return Device::DeviceErrorAsync; } if (action.actionTypeId() == sonosGroupRepeatActionTypeId) { - if (action.param(sonosGroupRepeatActionRepeatParamTypeId).value().toString() == "None") { - sonos->groupSetRepeat(groupId, Sonos::RepeatModeNone); - } else if (action.param(sonosGroupShuffleActionShuffleParamTypeId).value().toString() == "One") { - sonos->groupSetRepeat(groupId, Sonos::RepeatModeOne); - } else if (action.param(sonosGroupShuffleActionShuffleParamTypeId).value().toString() == "All") { - sonos->groupSetRepeat(groupId, Sonos::RepeatModeAll); + m_pendingActions.insert(sonos->groupSetRepeat(groupId, Sonos::RepeatModeNone), action.id()); + } else if (action.param(sonosGroupRepeatActionRepeatParamTypeId).value().toString() == "One") { + m_pendingActions.insert(sonos->groupSetRepeat(groupId, Sonos::RepeatModeOne), action.id()); + } else if (action.param(sonosGroupRepeatActionRepeatParamTypeId).value().toString() == "All") { + m_pendingActions.insert(sonos->groupSetRepeat(groupId, Sonos::RepeatModeAll), action.id()); } else { return Device::DeviceErrorHardwareFailure; } @@ -269,6 +304,13 @@ Device::DeviceError DevicePluginSonos::executeAction(Device *device, const Actio return Device::DeviceErrorAsync; } + + if (action.actionTypeId() == sonosGroupVolumeActionTypeId) { + int volume = action.param(sonosGroupVolumeActionVolumeParamTypeId).value().toInt(); + m_pendingActions.insert(sonos->setGroupVolume(groupId, volume), action.id()); + return Device::DeviceErrorAsync; + } + if (action.actionTypeId() == sonosGroupSkipNextActionTypeId) { m_pendingActions.insert(sonos->groupSkipToNextTrack(groupId), action.id()); return Device::DeviceErrorAsync; @@ -282,11 +324,11 @@ Device::DeviceError DevicePluginSonos::executeAction(Device *device, const Actio if (action.actionTypeId() == sonosGroupPlaybackStatusActionTypeId) { QString playbackStatus = action.param(sonosGroupPlaybackStatusActionPlaybackStatusParamTypeId).value().toString(); if (playbackStatus == "Playing") { - sonos->groupPlay(groupId); + m_pendingActions.insert(sonos->groupPlay(groupId), action.id()); } else if(playbackStatus == "Stopped") { - sonos->groupPause(groupId); + m_pendingActions.insert(sonos->groupPause(groupId), action.id()); } else if(playbackStatus == "Paused") { - sonos->groupPause(groupId); + m_pendingActions.insert(sonos->groupPause(groupId), action.id()); } return Device::DeviceErrorAsync; } @@ -295,28 +337,6 @@ Device::DeviceError DevicePluginSonos::executeAction(Device *device, const Actio return Device::DeviceErrorDeviceClassNotFound; } -void DevicePluginSonos::onPluginTimer() -{ - foreach (Device *device, myDevices().filterByDeviceClassId(sonosConnectionDeviceClassId)) { - - //get groups for each household in order to add or remove groups - Sonos *sonos = m_sonosConnections.value(device); - if (!sonos) - continue; - sonos->getHouseholds(); - - foreach (Device *groupDevice, myDevices().filterByParentDeviceId(device->id())) { - if (device->deviceClassId() == sonosGroupDeviceClassId) { - //get playback status of each group - QByteArray groupId = groupDevice->paramValue(sonosGroupDeviceGroupIdParamTypeId).toByteArray(); - sonos->getGroupPlaybackStatus(groupId); - sonos->getGroupMetadataStatus(groupId); - sonos->getGroupVolume(groupId); - } - } - } -} - void DevicePluginSonos::onConnectionChanged(bool connected) { Sonos *sonos = static_cast(sender()); @@ -354,6 +374,10 @@ void DevicePluginSonos::onRefreshTimeout() 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"; + return; + } m_tokenRefreshTimer->start((expireTime - 20) * 1000); } }); @@ -364,6 +388,34 @@ void DevicePluginSonos::onHouseholdIdsReceived(QList householdIds) Sonos *sonos = static_cast(sender()); foreach(QString householdId, householdIds) { sonos->getGroups(householdId); + sonos->getFavorites(householdId); + sonos->getPlaylists(householdId); + } +} + +void DevicePluginSonos::onFavouritesReceived(const QString &householdId, QList favourites) +{ + Q_UNUSED(householdId); + foreach(Sonos::FavouriteObject favourite, favourites) { + qDebug(dcSonos()) << "Favourite: " << favourite.name << favourite.description; + } +} + +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); + } +} + +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; } } @@ -376,27 +428,54 @@ void DevicePluginSonos::onGroupsReceived(QList groupObjects) QList deviceDescriptors; foreach(Sonos::GroupObject groupObject, groupObjects) { - if (!myDevices().filterByParam(sonosGroupDeviceGroupIdParamTypeId, groupObject.groupId).isEmpty()) { - continue; + Device *groupDevice = myDevices().findByParams(ParamList() << Param(sonosGroupDeviceGroupIdParamTypeId, groupObject.groupId)); + if (groupDevice) { + 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)); + deviceDescriptor.setParams(params); + deviceDescriptors.append(deviceDescriptor); } - DeviceDescriptor deviceDescriptor(sonosGroupDeviceClassId, groupObject.displayName, "Sonos Group", parentDevice->id()); - ParamList params; - params.append(Param(sonosGroupDeviceGroupIdParamTypeId, groupObject.groupId)); - deviceDescriptor.setParams(params); - deviceDescriptors.append(deviceDescriptor); } if (!deviceDescriptors.isEmpty()) emit autoDevicesAppeared(sonosGroupDeviceClassId, 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) { - qDebug(dcSonos()) << "Playback status received" << playBack.playbackState; 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"); @@ -424,7 +503,7 @@ void DevicePluginSonos::onMetadataStatusReceived(const QString &groupId, Sonos:: device->setStateValue(sonosGroupArtworkStateTypeId, metaDataStatus.container.imageUrl); } -void DevicePluginSonos::onVolumeReceived(const QString &groupId, Sonos::GroupVolumeObject groupVolume) +void DevicePluginSonos::onVolumeReceived(const QString &groupId, Sonos::VolumeObject groupVolume) { Device *device = myDevices().findByParams(ParamList() << Param(sonosGroupDeviceGroupIdParamTypeId, groupId)); if (!device) diff --git a/sonos/devicepluginsonos.h b/sonos/devicepluginsonos.h index 5820bd73..7d0cd69e 100644 --- a/sonos/devicepluginsonos.h +++ b/sonos/devicepluginsonos.h @@ -52,7 +52,8 @@ public: Device::DeviceError executeAction(Device *device, const Action &action) override; private: - PluginTimer *m_pluginTimer = nullptr; + PluginTimer *m_pluginTimer5sec = nullptr; + PluginTimer *m_pluginTimer60sec = nullptr; QTimer *m_tokenRefreshTimer = nullptr; QHash m_sonosConnections; @@ -63,15 +64,18 @@ private: QHash m_pendingActions; private slots: - void onPluginTimer(); void onConnectionChanged(bool connected); void onRefreshTimeout(); void onHouseholdIdsReceived(QList householdIds); + void onFavouritesReceived(const QString &householdId, QList favourites); + void onPlaylistsReceived(const QString &householdId, QList playlists); + void onPlaylistSummaryReceived(const QString &householdId, Sonos::PlaylistSummaryObject playlistSummary); + void onGroupsReceived(QList groupIds); void onPlayBackStatusReceived(const QString &groupId, Sonos::PlayBackObject playBack); void onMetadataStatusReceived(const QString &groupId, Sonos::MetadataStatus metaDataStatus); - void onVolumeReceived(const QString &groupId, Sonos::GroupVolumeObject groupVolume); + void onVolumeReceived(const QString &groupId, Sonos::VolumeObject groupVolume); void onActionExecuted(QUuid actionId, bool success); }; diff --git a/sonos/sonos.cpp b/sonos/sonos.cpp index 7aa13fe6..9544e078 100644 --- a/sonos/sonos.cpp +++ b/sonos/sonos.cpp @@ -76,28 +76,80 @@ void Sonos::getHouseholds() }); } -QUuid Sonos::cancelAudioClip() + +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_apiKey); + request.setUrl(QUrl(m_baseControlUrl + "/groups/" + groupId + "/favourites")); QUuid actionId = QUuid::createUuid(); + + QJsonObject object; + object.insert("favoriteId", QJsonValue::fromVariant(favouriteId)); + QJsonDocument doc(object); + + QNetworkReply *reply = m_networkManager->post(request, doc.toBinaryData()); + 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) { + qCWarning(dcSonos()) << "Request error:" << status << reply->errorString(); + emit actionExecuted(actionId, false); + return; + } + + //TODO parse response + emit actionExecuted(actionId, true); + }); return actionId; } -QUuid Sonos::loadAudioClip() +void Sonos::getFavorites(const QString &householdId) { - QUuid actionId = QUuid::createUuid(); - return actionId; + QNetworkRequest request; + request.setHeader(QNetworkRequest::KnownHeaders::ContentTypeHeader, "application/json"); + request.setRawHeader("Authorization", "Bearer " + m_accessToken); + request.setRawHeader("X-Sonos-Api-Key", m_apiKey); + request.setUrl(QUrl(m_baseControlUrl + "/households/" + householdId + "/favorites")); + 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) { + qCWarning(dcSonos()) << "Request error:" << status << reply->errorString(); + return; + } + + //qDebug(dcSonos()) << "Received response from Sonos" << reply->readAll(); + QJsonDocument data = QJsonDocument::fromJson(reply->readAll()); + if (!data.isObject()) + return; + + if (!data["items"].isArray()) + return; + + QJsonArray array = data["items"].toArray(); + QList favourites + ; + foreach (const QJsonValue & value, array) { + QJsonObject itemObject = value.toObject(); + qDebug(dcSonos()) << "Item ID received:" << itemObject["id"].toString(); + FavouriteObject favourite; + favourite.id = itemObject["id"].toString(); + favourite.name = itemObject["name"].toString(); + favourite.description = itemObject["description"].toString(); + favourites.append(favourite); + } + emit favouritesReceived(householdId, favourites); + }); } -void Sonos::getFavorites() -{ - -} - -QUuid Sonos::loadFavorite() -{ - QUuid actionId = QUuid::createUuid(); - return actionId; -} void Sonos::getGroups(const QString &householdId) { @@ -139,27 +191,6 @@ void Sonos::getGroups(const QString &householdId) }); } -QUuid Sonos::createGroup(const QString &householdId, QList playerIds) -{ - Q_UNUSED(householdId) - Q_UNUSED(playerIds) - QUuid actionId = QUuid::createUuid(); - return actionId; -} - -QUuid Sonos::modifyGroupMembers() -{ - QUuid actionId = QUuid::createUuid(); - return actionId; -} - -QUuid Sonos::setGroupMembers(const QString &groupId) -{ - Q_UNUSED(groupId) - QUuid actionId = QUuid::createUuid(); - return actionId; -} - void Sonos::getGroupVolume(const QString &groupId) { QNetworkRequest request; @@ -183,13 +214,13 @@ void Sonos::getGroupVolume(const QString &groupId) if (!data.isObject()) return; - GroupVolumeObject groupVolume; + VolumeObject volume; - groupVolume.volume = data["volume"].toInt(); - groupVolume.muted = data["muted"].toBool(); - groupVolume.fixed = data["fixed"].toBool(); + volume.volume = data["volume"].toInt(); + volume.muted = data["muted"].toBool(); + volume.fixed = data["fixed"].toBool(); - emit volumeReceived(groupId, groupVolume); + emit volumeReceived(groupId, volume); }); } @@ -203,11 +234,12 @@ QUuid Sonos::setGroupVolume(const QString &groupId, int volume) QUuid actionId = QUuid::createUuid(); QJsonObject object; - object.insert("volume", QJsonValue::fromVariant(volume)); + object.insert("volume", volume); QJsonDocument doc(object); + qDebug(dcSonos()) << "Set volume:" << groupId << doc.toJson(QJsonDocument::Compact); - QNetworkReply *reply = m_networkManager->post(request, doc.toBinaryData()); - connect(reply, &QNetworkReply::finished, this, [reply, actionId, this] { + 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(); @@ -217,6 +249,7 @@ QUuid Sonos::setGroupVolume(const QString &groupId, int volume) emit actionExecuted(actionId, false); return; } + getGroupVolume(groupId); emit actionExecuted(actionId, true); }); return actionId; @@ -224,9 +257,6 @@ QUuid Sonos::setGroupVolume(const QString &groupId, int volume) QUuid Sonos::setGroupMute(const QString &groupId, bool mute) { - Q_UNUSED(groupId) - Q_UNUSED(mute) - QNetworkRequest request; request.setHeader(QNetworkRequest::KnownHeaders::ContentTypeHeader, "application/json"); request.setRawHeader("Authorization", "Bearer " + m_accessToken); @@ -235,11 +265,13 @@ QUuid Sonos::setGroupMute(const QString &groupId, bool mute) QUuid actionId = QUuid::createUuid(); QJsonObject object; - object.insert("muted", QJsonValue::fromVariant(mute)); + object.insert("muted", mute); QJsonDocument doc(object); - QNetworkReply *reply = m_networkManager->post(request, doc.toBinaryData()); - connect(reply, &QNetworkReply::finished, this, [reply, actionId, this] { + 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(); @@ -249,6 +281,7 @@ QUuid Sonos::setGroupMute(const QString &groupId, bool mute) emit actionExecuted(actionId, false); return; } + getGroupVolume(groupId); emit actionExecuted(actionId, true); }); return actionId; @@ -267,8 +300,10 @@ QUuid Sonos::setGroupRelativeVolume(const QString &groupId, int volumeDelta) object.insert("volumeDelta", QJsonValue::fromVariant(volumeDelta)); QJsonDocument doc(object); - QNetworkReply *reply = m_networkManager->post(request, doc.toBinaryData()); - connect(reply, &QNetworkReply::finished, this, [reply, actionId, this] { + 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(); @@ -278,6 +313,7 @@ QUuid Sonos::setGroupRelativeVolume(const QString &groupId, int volumeDelta) emit actionExecuted(actionId, false); return; } + getGroupVolume(groupId); emit actionExecuted(actionId, true); }); return actionId; @@ -305,32 +341,61 @@ void Sonos::getGroupPlaybackStatus(const QString &groupId) if (!data.isObject()) return; - PlayBackObject playBackObject; + PlayBackObject playBack; QJsonObject object = data.object(); - playBackObject.itemId = object["itemId"].toString(); - playBackObject.positionMillis = object["positionMillis"].toInt(); - playBackObject.previousItemId = object["previousItemId"].toInt(); - playBackObject.previousPositionMillis = object["previousPositionMillis"].toInt(); + 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")) { - playBackObject.playbackState = PlayBackStateBuffering; + playBack.playbackState = PlayBackStateBuffering; } else if (playBackState.contains("IDLE")) { - playBackObject.playbackState = PlayBackStateIdle; + playBack.playbackState = PlayBackStateIdle; } else if (playBackState.contains("PAUSE")) { - playBackObject.playbackState = PlayBackStatePause; + playBack.playbackState = PlayBackStatePause; } else if (playBackState.contains("PLAYING")) { - playBackObject.playbackState = PlayBackStatePlaying; + playBack.playbackState = PlayBackStatePlaying; } - playBackObject.isDucking = object["isDucking"].toBool(); - playBackObject.queueVersion = object["queueVersion"].toString(); - emit playBackStatusReceived(groupId, playBackObject); + 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) { - Q_UNUSED(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_apiKey); + 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) { + qCWarning(dcSonos()) << "Request error:" << status << reply->errorString(); + emit actionExecuted(actionId, false); + return; + } + getGroupVolume(groupId); + emit actionExecuted(actionId, true); + }); return actionId; } @@ -343,8 +408,10 @@ QUuid Sonos::groupPlay(const QString &groupId) 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, this] { + connect(reply, &QNetworkReply::finished, this, [reply, actionId, groupId, this] { reply->deleteLater(); int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); @@ -354,6 +421,7 @@ QUuid Sonos::groupPlay(const QString &groupId) emit actionExecuted(actionId, false); return; } + getGroupPlaybackStatus(groupId); emit actionExecuted(actionId, true); }); return actionId; @@ -368,8 +436,10 @@ QUuid Sonos::groupPause(const QString &groupId) 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, this] { + connect(reply, &QNetworkReply::finished, this, [reply, actionId, groupId, this] { reply->deleteLater(); int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); @@ -379,6 +449,7 @@ QUuid Sonos::groupPause(const QString &groupId) emit actionExecuted(actionId, false); return; } + getGroupPlaybackStatus(groupId); emit actionExecuted(actionId, true); }); return actionId; @@ -398,7 +469,7 @@ QUuid Sonos::groupSeek(const QString &groupId, int possitionMillis) object.insert("positionMillis", QJsonValue::fromVariant(possitionMillis)); QJsonDocument doc(object); - QNetworkReply *reply = m_networkManager->post(request, doc.toBinaryData()); + 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(); @@ -416,7 +487,6 @@ QUuid Sonos::groupSeek(const QString &groupId, int possitionMillis) QUuid Sonos::groupSeekRelative(const QString &groupId, int deltaMillis) { - Q_UNUSED(groupId) QNetworkRequest request; request.setHeader(QNetworkRequest::KnownHeaders::ContentTypeHeader, "application/json"); request.setRawHeader("Authorization", "Bearer " + m_accessToken); @@ -428,7 +498,7 @@ QUuid Sonos::groupSeekRelative(const QString &groupId, int deltaMillis) object.insert("deltaMillis", QJsonValue::fromVariant(deltaMillis)); QJsonDocument doc(object); - QNetworkReply *reply = m_networkManager->post(request, doc.toBinaryData()); + 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(); @@ -462,8 +532,8 @@ QUuid Sonos::groupSetPlayModes(const QString &groupId, PlayMode playMode) object.insert("playModes", playModesObject); QJsonDocument doc(object); - QNetworkReply *reply = m_networkManager->post(request, doc.toBinaryData()); - connect(reply, &QNetworkReply::finished, this, [reply, actionId, this] { + 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(); @@ -473,6 +543,7 @@ QUuid Sonos::groupSetPlayModes(const QString &groupId, PlayMode playMode) emit actionExecuted(actionId, false); return; } + getGroupPlaybackStatus(groupId); emit actionExecuted(actionId, true); }); return actionId; @@ -493,8 +564,8 @@ QUuid Sonos::groupSetShuffle(const QString &groupId, bool shuffle) object.insert("playModes", playModesObject); QJsonDocument doc(object); - QNetworkReply *reply = m_networkManager->post(request, doc.toBinaryData()); - connect(reply, &QNetworkReply::finished, this, [reply, actionId, this] { + 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(); @@ -504,6 +575,7 @@ QUuid Sonos::groupSetShuffle(const QString &groupId, bool shuffle) emit actionExecuted(actionId, false); return; } + getGroupPlaybackStatus(groupId); emit actionExecuted(actionId, true); }); return actionId; @@ -521,20 +593,23 @@ QUuid Sonos::groupSetRepeat(const QString &groupId, RepeatMode repeatMode) 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 == RepeatModeAll) { + } 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.toBinaryData()); - connect(reply, &QNetworkReply::finished, this, [reply, actionId, this] { + 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(); @@ -544,6 +619,7 @@ QUuid Sonos::groupSetRepeat(const QString &groupId, RepeatMode repeatMode) emit actionExecuted(actionId, false); return; } + getGroupPlaybackStatus(groupId); emit actionExecuted(actionId, true); }); return actionId; @@ -564,8 +640,8 @@ QUuid Sonos::groupSetCrossfade(const QString &groupId, bool crossfade) object.insert("playModes", playModesObject); QJsonDocument doc(object); - QNetworkReply *reply = m_networkManager->post(request, doc.toBinaryData()); - connect(reply, &QNetworkReply::finished, this, [reply, actionId, this] { + 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(); @@ -575,6 +651,7 @@ QUuid Sonos::groupSetCrossfade(const QString &groupId, bool crossfade) emit actionExecuted(actionId, false); return; } + getGroupPlaybackStatus(groupId); emit actionExecuted(actionId, true); }); return actionId; @@ -590,7 +667,7 @@ QUuid Sonos::groupSkipToNextTrack(const QString &groupId) QUuid actionId = QUuid::createUuid(); QNetworkReply *reply = m_networkManager->post(request, ""); - connect(reply, &QNetworkReply::finished, this, [reply, actionId, this] { + connect(reply, &QNetworkReply::finished, this, [reply, actionId, groupId, this] { reply->deleteLater(); int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); @@ -600,6 +677,7 @@ QUuid Sonos::groupSkipToNextTrack(const QString &groupId) actionExecuted(actionId, false); return; } + getGroupMetadataStatus(groupId); actionExecuted(actionId, true); }); return actionId; @@ -615,7 +693,7 @@ QUuid Sonos::groupSkipToPreviousTrack(const QString &groupId) QUuid actionId = QUuid::createUuid(); QNetworkReply *reply = m_networkManager->post(request, ""); - connect(reply, &QNetworkReply::finished, this, [reply, actionId, this] { + connect(reply, &QNetworkReply::finished, this, [reply, actionId, groupId, this] { reply->deleteLater(); int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); @@ -625,6 +703,7 @@ QUuid Sonos::groupSkipToPreviousTrack(const QString &groupId) actionExecuted(actionId, false); return; } + getGroupMetadataStatus(groupId); actionExecuted(actionId, true); }); return actionId; @@ -640,7 +719,7 @@ QUuid Sonos::groupTogglePlayPause(const QString &groupId) QUuid actionId = QUuid::createUuid(); QNetworkReply *reply = m_networkManager->post(request, ""); - connect(reply, &QNetworkReply::finished, this, [reply, actionId, this] { + connect(reply, &QNetworkReply::finished, this, [reply, actionId, groupId, this] { reply->deleteLater(); int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); @@ -650,6 +729,7 @@ QUuid Sonos::groupTogglePlayPause(const QString &groupId) actionExecuted(actionId, false); return; } + getGroupPlaybackStatus(groupId); actionExecuted(actionId, true); }); return actionId; @@ -693,7 +773,7 @@ void Sonos::getGroupMetadataStatus(const QString &groupId) container.service = service; } if (containerObject.contains("id")) { - qDebug(dcSonos()) << "Item ID" << containerObject.value("id").toString(); + //TODO parse ID } metaDataStatus.container = container; } @@ -727,7 +807,7 @@ void Sonos::getGroupMetadataStatus(const QString &groupId) track.service = service; } if (trackObject.contains("id")) { - qDebug(dcSonos()) << "Item ID" << trackObject.value("id").toString(); + //TODO parse id } track.type = trackObject["type"].toString(); @@ -770,7 +850,7 @@ void Sonos::getGroupMetadataStatus(const QString &groupId) track.service = service; } if (trackObject.contains("id")) { - qDebug(dcSonos()) << "Item ID" << trackObject.value("id").toString(); + //TODO parse id } track.type = trackObject["type"].toString(); track.name = trackObject["name"].toString(); @@ -788,57 +868,304 @@ void Sonos::getGroupMetadataStatus(const QString &groupId) void Sonos::getPlayerVolume(const QByteArray &playerId) { - Q_UNUSED(playerId) + QNetworkRequest request; + request.setHeader(QNetworkRequest::KnownHeaders::ContentTypeHeader, "application/json"); + request.setRawHeader("Authorization", "Bearer " + m_accessToken); + request.setRawHeader("X-Sonos-Api-Key", m_apiKey); + 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) { + qCWarning(dcSonos()) << "Request error:" << status << reply->errorString(); + return; + } + + //qDebug(dcSonos()) << "Received response from Sonos" << reply->readAll(); + QJsonDocument data = QJsonDocument::fromJson(reply->readAll()); + if (!data.isObject()) + return; + + VolumeObject volume; + volume.volume = data["volume"].toInt(); + volume.muted = data["muted"].toBool(); + volume.fixed = data["fixed"].toBool(); + emit playerVolumeReceived(playerId, volume); + }); } QUuid Sonos::setPlayerVolume(const QByteArray &playerId, int volume) { - Q_UNUSED(playerId) - Q_UNUSED(volume) + QNetworkRequest request; + request.setHeader(QNetworkRequest::KnownHeaders::ContentTypeHeader, "application/json"); + request.setRawHeader("Authorization", "Bearer " + m_accessToken); + request.setRawHeader("X-Sonos-Api-Key", m_apiKey); + 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) { + qCWarning(dcSonos()) << "Request error:" << status << reply->errorString(); + emit actionExecuted(actionId, false); + return; + } + getPlayerVolume(playerId); + emit actionExecuted(actionId, true); + }); return actionId; } QUuid Sonos::setPlayerRelativeVolume(const QByteArray &playerId, int volumeDelta) { - Q_UNUSED(playerId) - Q_UNUSED(volumeDelta) + QNetworkRequest request; + request.setHeader(QNetworkRequest::KnownHeaders::ContentTypeHeader, "application/json"); + request.setRawHeader("Authorization", "Bearer " + m_accessToken); + request.setRawHeader("X-Sonos-Api-Key", m_apiKey); + 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) { + qCWarning(dcSonos()) << "Request error:" << status << reply->errorString(); + emit actionExecuted(actionId, false); + return; + } + getPlayerVolume(playerId); + emit actionExecuted(actionId, true); + }); return actionId; } QUuid Sonos::setPlayerMute(const QByteArray &playerId, bool mute) { - Q_UNUSED(playerId) - Q_UNUSED(mute) + QNetworkRequest request; + request.setHeader(QNetworkRequest::KnownHeaders::ContentTypeHeader, "application/json"); + request.setRawHeader("Authorization", "Bearer " + m_accessToken); + request.setRawHeader("X-Sonos-Api-Key", m_apiKey); + 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) { + qCWarning(dcSonos()) << "Request error:" << status << reply->errorString(); + emit actionExecuted(actionId, false); + return; + } + getPlayerVolume(playerId); + emit actionExecuted(actionId, true); + }); return actionId; } -void Sonos::getPlaylist() +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_apiKey); + 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) { + qCWarning(dcSonos()) << "Request error:" << status << reply->errorString(); + return; + } + + //qDebug(dcSonos()) << "Received response from Sonos" << reply->readAll(); + QJsonDocument data = QJsonDocument::fromJson(reply->readAll()); + if (!data.isObject()) + return; + + if (!data["tracks"].isArray()) + return; + + PlaylistSummaryObject playlist; + QJsonArray array = data["tracks"].toArray(); + foreach (const QJsonValue & value, array) { + QJsonObject itemObject = value.toObject(); + qDebug(dcSonos()) << "Item ID received:" << itemObject["id"].toString(); + 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() +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_apiKey); + 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) { + qCWarning(dcSonos()) << "Request error:" << status << reply->errorString(); + return; + } + + //qDebug(dcSonos()) << "Received response from Sonos" << reply->readAll(); + QJsonDocument data = QJsonDocument::fromJson(reply->readAll()); + if (!data.isObject()) + return; + + if (!data["items"].isArray()) + return; + + QJsonArray array = data["playlists"].toArray(); + QList playlists; + foreach (const QJsonValue & value, array) { + QJsonObject itemObject = value.toObject(); + qDebug(dcSonos()) << "Item ID received:" << itemObject["id"].toString(); + 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() +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_apiKey); + 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) { + qCWarning(dcSonos()) << "Request error:" << status << reply->errorString(); + emit actionExecuted(actionId, false); + return; + } + emit actionExecuted(actionId, true); + }); return actionId; } -void Sonos::getPlayerSettings() +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_apiKey); + 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) { + qCWarning(dcSonos()) << "Request error:" << status << reply->errorString(); + return; + } + QJsonDocument data = QJsonDocument::fromJson(reply->readAll()); + if (!data.isObject()) + return; + + 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() +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_apiKey); + 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) { + qCWarning(dcSonos()) << "Request error:" << status << reply->errorString(); + emit actionExecuted(actionId, false); + return; + } + getPlayerSettings(playerId); + emit actionExecuted(actionId, true); + }); return actionId; } - diff --git a/sonos/sonos.h b/sonos/sonos.h index 541e345e..4ce327a5 100644 --- a/sonos/sonos.h +++ b/sonos/sonos.h @@ -53,10 +53,6 @@ public: bool crossfade; }; - struct PlayerObject { - - }; - /* Represents a Sonos household.*/ struct GroupObject { QString CoordinatorId; //Player acting as the group coordinator for the group @@ -66,17 +62,11 @@ public: QString displayName; //The display name for the group, such as “Living Room” or “Kitchen + 2”. }; - struct GroupVolumeObject { + 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. }; - /* - * Represents a Sonos household.*/ - /*struct HouseholdObject { - QString id; //Identifies a Sonos household. - QString name; //A user-displayable name of the Sonos household - };*/ /* * The music service identifier or a pseudo-service identifier in the case of local library. */ @@ -95,6 +85,7 @@ public: QString name; QString description; QString imageUrl; + ServiceObject service; }; struct PlaylistObject @@ -107,8 +98,8 @@ public: struct PlayerSettingsObject { - int volumeMode; - float volumeScalingFactor; + QString volumeMode; + double volumeScalingFactor; bool monoMode; bool wifiDisabled; }; @@ -173,8 +164,7 @@ public: * 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 - { + struct TrackObject { QString type; QString name; QString imageUrl; @@ -190,8 +180,7 @@ public: /* 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 - { + struct ItemObject { QString itemId; TrackObject track; bool deleted; @@ -204,21 +193,28 @@ public: 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 &accessToken, QObject *parent = nullptr); void setAccessToken(const QByteArray &accessToken); void getHouseholds(); - void getFavorites(); + void getFavorites(const QString &householdId); void getGroups(const QString &householdId); - QUuid cancelAudioClip(); - QUuid loadAudioClip(); - QUuid loadFavorite(); - - QUuid createGroup(const QString &householdId, QList playerIds); - QUuid modifyGroupMembers(); - QUuid setGroupMembers(const QString &groupId); + QUuid loadFavorite(const QString &groupId, const QString &favouriteId); //Group volume void getGroupVolume(const QString &groupId); //Get the volume and mute state of a group. @@ -254,13 +250,13 @@ public: QUuid setPlayerMute(const QByteArray &playerId, bool mute); //Playlists API namespace - void getPlaylist(); - void getPlaylists(); - QUuid loadPlaylist(); + void getPlaylists(const QString &householdId); + void getPlaylist(const QString &householdId, const QString &playlistId); + QUuid loadPlaylist(const QString &groupId, const QString &playlistId); //Settings - void getPlayerSettings(); - QUuid setPlayerSettings(); + void getPlayerSettings(const QString &playerId); + QUuid setPlayerSettings(const QString &playerId, PlayerSettingsObject settings); private: QByteArray m_baseAuthorizationUrl = "https://api.sonos.com/login/v3/oauth"; @@ -275,12 +271,17 @@ private slots: signals: void connectionChanged(bool connected); void householdIdsReceived(QList householdIds); + void favouritesReceived(const QString &householdId, QList favourites); + void playlistsReceived(const QString &householdId, QList playlists); void groupsReceived(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, GroupVolumeObject groupVolume); + 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); }; From cd17134c1967214276183bc3b51c7b524b16ce91 Mon Sep 17 00:00:00 2001 From: nymea Date: Fri, 6 Sep 2019 00:26:10 +0200 Subject: [PATCH 12/22] moved authentication into sonos class --- .gitmodules | 3 - sonos/devicepluginsonos.cpp | 140 +++++++--------------- sonos/devicepluginsonos.h | 9 +- sonos/devicepluginsonos.json | 8 ++ sonos/oauth.cpp | 221 ----------------------------------- sonos/oauth.h | 71 ----------- sonos/sonos.cpp | 202 +++++++++++++++++++++++++------- sonos/sonos.h | 41 ++++--- sonos/sonos.pro | 2 - 9 files changed, 243 insertions(+), 454 deletions(-) delete mode 100644 sonos/oauth.cpp delete mode 100644 sonos/oauth.h diff --git a/.gitmodules b/.gitmodules index 7581b25b..e69de29b 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +0,0 @@ -[submodule "sonos/sonos/noson"] - path = sonos/sonos/noson - url = https://github.com/janbar/noson.git diff --git a/sonos/devicepluginsonos.cpp b/sonos/devicepluginsonos.cpp index 5441b669..02bb5be6 100644 --- a/sonos/devicepluginsonos.cpp +++ b/sonos/devicepluginsonos.cpp @@ -32,7 +32,6 @@ DevicePluginSonos::DevicePluginSonos() { - } @@ -83,20 +82,22 @@ Device::DeviceSetupStatus DevicePluginSonos::setupDevice(Device *device) }); } - if(!m_tokenRefreshTimer) { - m_tokenRefreshTimer = new QTimer(this); - m_tokenRefreshTimer->setSingleShot(false); - connect(m_tokenRefreshTimer, &QTimer::timeout, this, &DevicePluginSonos::onRefreshTimeout); - } - if (device->deviceClassId() == sonosConnectionDeviceClassId) { qCDebug(dcSonos()) << "Sonos OAuth setup complete"; + Sonos *sonos; + if (m_setupSonosConnections.keys().contains(device->id())) { + //Fresh device setup, has already a fresh access token + sonos = m_setupSonosConnections.take(device->id()); + } 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(); - pluginStorage()->beginGroup(device->id().toString()); - QByteArray accessToken = pluginStorage()->value("access_token").toByteArray(); - pluginStorage()->endGroup(); + sonos = new Sonos(hardwareManager()->networkManager(), "b15cbf8c-a39c-47aa-bd93-635a96e9696c", "c086ba71-e562-430b-a52f-867c6482fd11", "", this); + sonos->getAccessTokenFromRefreshToken(refreshToken); + } - Sonos *sonos = new Sonos(hardwareManager()->networkManager(), accessToken, this); connect(sonos, &Sonos::connectionChanged, this, &DevicePluginSonos::onConnectionChanged); connect(sonos, &Sonos::householdIdsReceived, this, &DevicePluginSonos::onHouseholdIdsReceived); connect(sonos, &Sonos::groupsReceived, this, &DevicePluginSonos::onGroupsReceived); @@ -115,22 +116,13 @@ Device::DeviceSetupStatus DevicePluginSonos::setupDevice(Device *device) DevicePairingInfo DevicePluginSonos::pairDevice(DevicePairingInfo &devicePairingInfo) { if (devicePairingInfo.deviceClassId() == sonosConnectionDeviceClassId) { - QString clientId = "b15cbf8c-a39c-47aa-bd93-635a96e9696c"; - QString clientSecret = "c086ba71-e562-430b-a52f-867c6482fd11"; - - QUrl url("https://api.sonos.com/login/v3/oauth"); - QUrlQuery queryParams; - queryParams.addQueryItem("client_id", clientId); - queryParams.addQueryItem("redirect_uri", "https://127.0.0.1:8888"); - queryParams.addQueryItem("response_type", "code"); - queryParams.addQueryItem("scope", "playback-control-all"); - queryParams.addQueryItem("state", QUuid::createUuid().toString()); - url.setQuery(queryParams); + Sonos *sonos = new Sonos(hardwareManager()->networkManager(), "b15cbf8c-a39c-47aa-bd93-635a96e9696c", "c086ba71-e562-430b-a52f-867c6482fd11", "", this); + QUrl url = sonos->getLoginUrl(QUrl("https://127.0.0.1:8000")); qCDebug(dcSonos()) << "Sonos url:" << url; - devicePairingInfo.setOAuthUrl(url); devicePairingInfo.setStatus(Device::DeviceErrorNoError); + m_setupSonosConnections.insert(devicePairingInfo.deviceId(), sonos); return devicePairingInfo; } @@ -148,61 +140,39 @@ DevicePairingInfo DevicePluginSonos::confirmPairing(DevicePairingInfo &devicePai qCDebug(dcSonos()) << "Secret is" << secret; QUrl url(secret); QUrlQuery query(url); - qCDebug(dcSonos()) << "Acess code is:" << query.queryItemValue("code"); + QByteArray accessCode = query.queryItemValue("code").toLocal8Bit(); + qCDebug(dcSonos()) << "Acess code is:" << accessCode; - QString accessCode = query.queryItemValue("code"); + Sonos *sonos = m_setupSonosConnections.value(devicePairingInfo.deviceId()); - // Obtaining access token - url = QUrl("https://api.sonos.com/login/v3/oauth/access"); - query.clear(); - query.addQueryItem("grant_type", "authorization_code"); - query.addQueryItem("code", accessCode); - query.addQueryItem("redirect_uri", "https%3A%2F%2F127.0.0.1%3A8888"); - url.setQuery(query); - - QNetworkRequest request(url); - request.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded;charset=utf-8"); - - QByteArray clientId = "b15cbf8c-a39c-47aa-bd93-635a96e9696c"; - QByteArray clientSecret = "c086ba71-e562-430b-a52f-867c6482fd11"; - - QByteArray auth = QByteArray(clientId + ':' + clientSecret).toBase64(QByteArray::Base64Encoding | QByteArray::KeepTrailingEquals); - request.setRawHeader("Authorization", QString("Basic %1").arg(QString(auth)).toUtf8()); - - QNetworkReply *reply = hardwareManager()->networkManager()->post(request, QByteArray()); - connect(reply, &QNetworkReply::finished, this, [this, reply, devicePairingInfo](){ - reply->deleteLater(); + if (!sonos) { + m_setupSonosConnections.remove(devicePairingInfo.deviceId()); + sonos->deleteLater(); + devicePairingInfo.setStatus(Device::DeviceErrorHardwareFailure); + return devicePairingInfo; + } + sonos->getAccessTokenFromAuthorizationCode(accessCode); + connect(sonos, &Sonos::authenticationStatusChanged, this, [devicePairingInfo, this](bool authenticated){ + Sonos *sonos = static_cast(sender()); DevicePairingInfo info(devicePairingInfo); - - QJsonDocument jsonDoc = QJsonDocument::fromJson(reply->readAll()); - 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") ) { + if(!authenticated) { + m_setupSonosConnections.remove(info.deviceId()); + sonos->deleteLater(); info.setStatus(Device::DeviceErrorSetupFailed); emit pairingFinished(info); return; } - qCDebug(dcSonos()) << "Access token:" << jsonDoc.toVariant().toMap().value("access_token").toString(); - QByteArray accessToken = jsonDoc.toVariant().toMap().value("access_token").toByteArray(); - - qCDebug(dcSonos()) << "Refresh token:" << jsonDoc.toVariant().toMap().value("refresh_token").toString(); - QByteArray refreshToken = jsonDoc.toVariant().toMap().value("refresh_token").toByteArray(); + QByteArray accessToken = sonos->accessToken(); + QByteArray refreshToken = sonos->refreshToken(); pluginStorage()->beginGroup(info.deviceId().toString()); pluginStorage()->setValue("access_token", accessToken); pluginStorage()->setValue("refresh_token", refreshToken); pluginStorage()->endGroup(); - 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(); - //m_tokenRefreshTimer->start((expireTime - 20) * 1000); - //TODO - } - info.setStatus(Device::DeviceErrorNoError); emit pairingFinished(info); }); - devicePairingInfo.setStatus(Device::DeviceErrorAsync); return devicePairingInfo; } @@ -348,39 +318,18 @@ void DevicePluginSonos::onConnectionChanged(bool connected) } } -void DevicePluginSonos::onRefreshTimeout() +void DevicePluginSonos::onAuthenticationStatusChanged(bool authenticated) { - qCDebug(dcSonos) << "Refresh authentication token"; - - QUrlQuery query; - query.addQueryItem("grant_type", "refresh_token"); - query.addQueryItem("refresh_token", m_sonosConnectionRefreshToken); - - QUrl url("https://api.sonos.com/login/v3/oauth"); - QNetworkRequest request(url); - request.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded; charset=UTF-8"); - QNetworkReply *reply = hardwareManager()->networkManager()->post(request, QByteArray()); - connect(reply, &QNetworkReply::finished, this, [this, reply](){ - reply->deleteLater(); - - QJsonDocument jsonDoc = QJsonDocument::fromJson(reply->readAll()); - qCDebug(dcSonos()) << "Sonos accessToken reply:" << this << reply->error() << reply->errorString() << jsonDoc.toJson(); - if(!jsonDoc.toVariant().toMap().contains("access_token")) { - return; - } - qCDebug(dcSonos()) << "Access token:" << jsonDoc.toVariant().toMap().value("access_token").toString(); - m_sonosConnectionAccessToken = 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()) << "expires at" << QDateTime::currentDateTime().addSecs(expireTime).toString(); - if (!m_tokenRefreshTimer) { - qWarning(dcSonos()) << "Token refresh timer not initialized"; - return; - } - m_tokenRefreshTimer->start((expireTime - 20) * 1000); - } - }); + Sonos *sonosConnection = static_cast(sender()); + Device *device = m_sonosConnections.key(sonosConnection); + 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) @@ -419,8 +368,9 @@ void DevicePluginSonos::onPlaylistSummaryReceived(const QString &householdId, So } } -void DevicePluginSonos::onGroupsReceived(QList groupObjects) +void DevicePluginSonos::onGroupsReceived(const QString &householdId, QList groupObjects) { + Q_UNUSED(householdId); Sonos *sonos = static_cast(sender()); Device *parentDevice = m_sonosConnections.key(sonos); if (!parentDevice) diff --git a/sonos/devicepluginsonos.h b/sonos/devicepluginsonos.h index 7d0cd69e..fcf2eb38 100644 --- a/sonos/devicepluginsonos.h +++ b/sonos/devicepluginsonos.h @@ -29,8 +29,6 @@ #include #include -#include - class DevicePluginSonos : public DevicePlugin { @@ -54,8 +52,8 @@ public: private: PluginTimer *m_pluginTimer5sec = nullptr; PluginTimer *m_pluginTimer60sec = nullptr; - QTimer *m_tokenRefreshTimer = nullptr; + QHash m_setupSonosConnections; QHash m_sonosConnections; QList m_householdIds; @@ -63,16 +61,17 @@ private: QByteArray m_sonosConnectionRefreshToken; QHash m_pendingActions; + private slots: void onConnectionChanged(bool connected); - void onRefreshTimeout(); + void onAuthenticationStatusChanged(bool authenticated); void onHouseholdIdsReceived(QList householdIds); void onFavouritesReceived(const QString &householdId, QList favourites); void onPlaylistsReceived(const QString &householdId, QList playlists); void onPlaylistSummaryReceived(const QString &householdId, Sonos::PlaylistSummaryObject playlistSummary); - void onGroupsReceived(QList groupIds); + 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); diff --git a/sonos/devicepluginsonos.json b/sonos/devicepluginsonos.json index af19f4d0..cfbc056d 100644 --- a/sonos/devicepluginsonos.json +++ b/sonos/devicepluginsonos.json @@ -26,6 +26,14 @@ "displayNameEvent": "connected changed", "defaultValue": false, "type": "bool" + }, + { + "id": "48b5c1bf-7df0-45d0-9ba3-290fc3acddc3", + "name": "loggedIn", + "displayName": "Logged in", + "displayNameEvent": "Logged in changed", + "defaultValue": false, + "type": "bool" } ] }, diff --git a/sonos/oauth.cpp b/sonos/oauth.cpp deleted file mode 100644 index 2c34c2b9..00000000 --- a/sonos/oauth.cpp +++ /dev/null @@ -1,221 +0,0 @@ -#include "oauth.h" -#include "extern-plugininfo.h" - -#include -#include -#include - -OAuth::OAuth(QString clientId, QObject *parent) : - QObject(parent), - m_clientId(clientId), - m_authenticated(false) -{ - m_networkManager = new QNetworkAccessManager(this); - connect(m_networkManager, &QNetworkAccessManager::finished, this, &OAuth::replyFinished); - - m_timer = new QTimer(this); - m_timer->setSingleShot(false); - - connect(m_timer, &QTimer::timeout, this, &OAuth::refreshTimeout); -} - -QUrl OAuth::url() const -{ - return m_url; -} - -void OAuth::setUrl(const QUrl &url) -{ - m_url = url; -} - -QUrlQuery OAuth::query() const -{ - return m_query; -} - -void OAuth::setQuery(const QUrlQuery &query) -{ - m_query = query; -} - -QString OAuth::clientId() const -{ - return m_clientId; -} - -void OAuth::setClientId(const QString &clientId) -{ - m_clientId = clientId; -} - -QString OAuth::scope() const -{ - return m_scope; -} - -void OAuth::setScope(const QString &scope) -{ - m_scope = scope; -} - -QString OAuth::authorizationCode() const -{ - return m_token; -} - -QString OAuth::bearerToken() const -{ - return m_token; -} - -bool OAuth::authenticated() const -{ - return m_authenticated; -} - -void OAuth::startAuthentication() -{ - qCDebug(dcSonos) << "Start authentication"; - - QUrlQuery query; - query.addQueryItem("client_id", m_clientId); - query.addQueryItem("redirect_uri", m_redirectUri); - query.addQueryItem("response_type", "code"); - query.addQueryItem("scope", m_scope); - m_state = QUuid().toByteArray(); - query.addQueryItem("state", m_state); - setQuery(query); - - QNetworkRequest request(m_url); - request.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded; charset=UTF-8"); - m_tokenRequests.append(m_networkManager->post(request, m_query.toString().toUtf8())); -} - -void OAuth::setAuthenticated(const bool &authenticated) -{ - if (authenticated) { - qCDebug(dcSonos()) << "Authenticated successfully"; - } else { - m_timer->stop(); - qCWarning(dcSonos) << "Authentication failed"; - } - m_authenticated = authenticated; - emit authenticationChanged(); -} - -void OAuth::setToken(const QString &token) -{ - m_token = token; - emit tokenChanged(); -} - -void OAuth::replyFinished(QNetworkReply *reply) -{ - int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); - reply->deleteLater(); - // token request - if (m_tokenRequests.contains(reply)) { - - QByteArray data = reply->readAll(); - m_tokenRequests.removeAll(reply); - - // check HTTP status code - if (status != 200) { - qCWarning(dcSonos) << "Request token reply HTTP error:" << status << reply->errorString(); - qCWarning(dcSonos) << data; - setAuthenticated(false); - return; - } - - // check JSON - QJsonParseError error; - QJsonDocument jsonDoc = QJsonDocument::fromJson(data, &error); - if (error.error != QJsonParseError::NoError) { - qCWarning(dcSonos) << "Request token reply JSON error:" << error.errorString(); - setAuthenticated(false); - return; - } - - if (!jsonDoc.toVariant().toMap().contains("code")) { - qCWarning(dcSonos) << "Could not get code" << jsonDoc.toJson(); - setAuthenticated(false); - return; - } - - if (!jsonDoc.toVariant().toMap().contains("state")) { - qCWarning(dcSonos) << "Could not get state" << jsonDoc.toJson(); - return; - } - - if (jsonDoc.toVariant().toMap().value("state").toString() != m_state) { - qCWarning(dcSonos) << "State doesn't match. Expected:" << m_state << "Received:" << jsonDoc.toVariant().toMap().value("state").toString(); - } - - setToken(jsonDoc.toVariant().toMap().value("code").toString()); - setAuthenticated(true); - - if (jsonDoc.toVariant().toMap().contains("expires_in") && jsonDoc.toVariant().toMap().contains("refresh_token")) { - int expireTime = jsonDoc.toVariant().toMap().value("expires_in").toInt(); - m_refreshToken = jsonDoc.toVariant().toMap().value("refresh_token").toString(); - qCDebug(dcSonos) << "Token will be refreshed in" << expireTime << "[s]"; - m_timer->start((expireTime - 20) * 1000); - } - - } else if (m_refreshTokenRequests.contains(reply)) { - - QByteArray data = reply->readAll(); - m_refreshTokenRequests.removeAll(reply); - - // check HTTP status code - if (status != 200) { - qCWarning(dcSonos) << "Refresh token reply HTTP error:" << status << reply->errorString(); - qCWarning(dcSonos) << data; - setAuthenticated(false); - return; - } - - // check JSON - QJsonParseError error; - QJsonDocument jsonDoc = QJsonDocument::fromJson(data, &error); - if (error.error != QJsonParseError::NoError) { - qCWarning(dcSonos) << "Refresh token reply JSON error:" << error.errorString(); - setAuthenticated(false); - return; - } - - if (!jsonDoc.toVariant().toMap().contains("access_token")) { - qCWarning(dcSonos) << "Could not get access token after refresh" << jsonDoc.toJson(); - setAuthenticated(false); - return; - } - - - setToken(jsonDoc.toVariant().toMap().value("access_token").toString()); - qCDebug(dcSonos) << "Token refreshed successfully"; - - if (jsonDoc.toVariant().toMap().contains("expires_in") && jsonDoc.toVariant().toMap().contains("refresh_token")) { - int expireTime = jsonDoc.toVariant().toMap().value("expires_in").toInt(); - m_refreshToken = jsonDoc.toVariant().toMap().value("refresh_token").toString(); - qCDebug(dcSonos) << "Token will be refreshed in" << expireTime << "[s]"; - m_timer->start((expireTime - 20) * 1000); - } - - if (!authenticated()) - setAuthenticated(true); - } -} - -void OAuth::refreshTimeout() -{ - qCDebug(dcSonos) << "Refresh authentication token for"; - - QUrlQuery query; - query.addQueryItem("grant_type", "refresh_token"); - query.addQueryItem("refresh_token", m_refreshToken); - query.addQueryItem("client_id", m_clientId); - - QNetworkRequest request(m_url); - request.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded; charset=UTF-8"); - m_refreshTokenRequests.append(m_networkManager->post(request, query.toString().toUtf8())); -} diff --git a/sonos/oauth.h b/sonos/oauth.h deleted file mode 100644 index 205eda51..00000000 --- a/sonos/oauth.h +++ /dev/null @@ -1,71 +0,0 @@ -#ifndef OAUTH_H -#define OAUTH_H - - -#include -#include -#include -#include -#include -#include -#include - -class OAuth : public QObject -{ - Q_OBJECT - -public: - explicit OAuth(QString clientId, QObject *parent = nullptr); - - QUrl url() const; - void setUrl(const QUrl &url); - - QUrlQuery query() const; - void setQuery(const QUrlQuery &query); - - QString clientId() const; - void setClientId(const QString &clientId); - - QString scope() const; - void setScope(const QString &scope); - - QByteArray authorizationCode() const; - QByteArray bearerToken() const; - - bool authenticated() const; - - void startAuthentication(); - -private: - - QNetworkAccessManager *m_networkManager; - QTimer *m_timer; - QList m_tokenRequests; - QList m_refreshTokenRequests; - - QUrl m_url; - QUrlQuery m_query; - QString m_clientId; - QString m_scope; - QString m_state; - QString m_redirectUri; - QString m_responseType; - - QString m_token; - QString m_refreshToken; - - bool m_authenticated; - - void setAuthenticated(const bool &authenticated); - void setToken(const QString &token); - -private slots: - void replyFinished(QNetworkReply *reply); - void refreshTimeout(); - -signals: - void authenticationChanged(); - void tokenChanged(); -}; - -#endif // OAUTH_H diff --git a/sonos/sonos.cpp b/sonos/sonos.cpp index 9544e078..6fc27dba 100644 --- a/sonos/sonos.cpp +++ b/sonos/sonos.cpp @@ -26,17 +26,46 @@ #include #include #include +#include -Sonos::Sonos(NetworkAccessManager *networkmanager, const QByteArray &accessToken, QObject *parent) : +Sonos::Sonos(NetworkAccessManager *networkmanager, const QByteArray &clientKey, const QByteArray &clientSecret, const QByteArray &refreshToken, QObject *parent) : QObject(parent), - m_accessToken(accessToken), + m_clientKey(clientKey), + m_clientSecret(clientSecret), + m_refreshToken(refreshToken), m_networkManager(networkmanager) { + if(!m_tokenRefreshTimer) { + m_tokenRefreshTimer = new QTimer(this); + m_tokenRefreshTimer->setSingleShot(true); + connect(m_tokenRefreshTimer, &QTimer::timeout, this, &Sonos::onRefreshTimeout); + } } -void Sonos::setAccessToken(const QByteArray &accessToken) +QUrl Sonos::getLoginUrl(const QUrl &redirectUrl) { - m_accessToken = accessToken; + QString clientId = "b15cbf8c-a39c-47aa-bd93-635a96e9696c"; + + QUrl url("https://api.sonos.com/login/v3/oauth"); + QUrlQuery queryParams; + queryParams.addQueryItem("client_id", clientId); + queryParams.addQueryItem("redirect_uri", redirectUrl.toString()); + 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() @@ -44,7 +73,7 @@ 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_apiKey); + 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"); @@ -53,6 +82,12 @@ void Sonos::getHouseholds() int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); connectionChanged(true); + + if (status == 401) { + //Authentication required + getAccessTokenFromRefreshToken(m_refreshToken); + return; + } // Check HTTP status code if (status != 200 || reply->error() != QNetworkReply::NoError) { qCWarning(dcSonos()) << "Request error:" << status << reply->errorString(); @@ -82,7 +117,7 @@ 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_apiKey); + request.setRawHeader("X-Sonos-Api-Key", m_clientKey); request.setUrl(QUrl(m_baseControlUrl + "/groups/" + groupId + "/favourites")); QUuid actionId = QUuid::createUuid(); @@ -90,7 +125,7 @@ QUuid Sonos::loadFavorite(const QString &groupId, const QString &favouriteId) object.insert("favoriteId", QJsonValue::fromVariant(favouriteId)); QJsonDocument doc(object); - QNetworkReply *reply = m_networkManager->post(request, doc.toBinaryData()); + 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(); @@ -101,8 +136,6 @@ QUuid Sonos::loadFavorite(const QString &groupId, const QString &favouriteId) emit actionExecuted(actionId, false); return; } - - //TODO parse response emit actionExecuted(actionId, true); }); return actionId; @@ -113,7 +146,7 @@ void 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_apiKey); + request.setRawHeader("X-Sonos-Api-Key", m_clientKey); request.setUrl(QUrl(m_baseControlUrl + "/households/" + householdId + "/favorites")); QNetworkReply *reply = m_networkManager->get(request); connect(reply, &QNetworkReply::finished, this, [reply, householdId, this] { @@ -126,7 +159,6 @@ void Sonos::getFavorites(const QString &householdId) return; } - //qDebug(dcSonos()) << "Received response from Sonos" << reply->readAll(); QJsonDocument data = QJsonDocument::fromJson(reply->readAll()); if (!data.isObject()) return; @@ -156,10 +188,10 @@ 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_apiKey); + 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, this] { + connect(reply, &QNetworkReply::finished, this, [reply, householdId, this] { reply->deleteLater(); int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); @@ -187,7 +219,7 @@ void Sonos::getGroups(const QString &householdId) group.displayName = obj["name"].toString(); groupObjects.append(group); } - emit groupsReceived(groupObjects); + emit groupsReceived(householdId, groupObjects); }); } @@ -196,7 +228,7 @@ 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_apiKey); + 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] { @@ -229,7 +261,7 @@ 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_apiKey); + request.setRawHeader("X-Sonos-Api-Key", m_clientKey); request.setUrl(QUrl(m_baseControlUrl + "/groups/" + groupId + "/groupVolume")); QUuid actionId = QUuid::createUuid(); @@ -260,7 +292,7 @@ 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_apiKey); + request.setRawHeader("X-Sonos-Api-Key", m_clientKey); request.setUrl(QUrl(m_baseControlUrl + "/groups/" + groupId + "/groupVolume/mute")); QUuid actionId = QUuid::createUuid(); @@ -292,7 +324,7 @@ 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_apiKey); + request.setRawHeader("X-Sonos-Api-Key", m_clientKey); request.setUrl(QUrl(m_baseControlUrl + "/groups/" + groupId + "/groupVolume/relative")); QUuid actionId = QUuid::createUuid(); @@ -324,7 +356,7 @@ 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_apiKey); + 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] { @@ -378,7 +410,7 @@ QUuid Sonos::groupLoadLineIn(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_apiKey); + request.setRawHeader("X-Sonos-Api-Key", m_clientKey); request.setUrl(QUrl(m_baseControlUrl + "/groups/" + groupId + "/playback/lineIn")); QUuid actionId = QUuid::createUuid(); @@ -404,7 +436,7 @@ 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_apiKey); + request.setRawHeader("X-Sonos-Api-Key", m_clientKey); request.setUrl(QUrl(m_baseControlUrl + "/groups/" + groupId + "/playback/play")); QUuid actionId = QUuid::createUuid(); @@ -432,7 +464,7 @@ 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_apiKey); + request.setRawHeader("X-Sonos-Api-Key", m_clientKey); request.setUrl(QUrl(m_baseControlUrl + "/groups/" + groupId + "/playback/pause")); QUuid actionId = QUuid::createUuid(); @@ -461,7 +493,7 @@ QUuid Sonos::groupSeek(const QString &groupId, int possitionMillis) QNetworkRequest request; request.setHeader(QNetworkRequest::KnownHeaders::ContentTypeHeader, "application/json"); request.setRawHeader("Authorization", "Bearer " + m_accessToken); - request.setRawHeader("X-Sonos-Api-Key", m_apiKey); + request.setRawHeader("X-Sonos-Api-Key", m_clientKey); request.setUrl(QUrl(m_baseControlUrl + "/groups/" + groupId + "/playback/seek")); QUuid actionId = QUuid::createUuid(); @@ -490,7 +522,7 @@ 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_apiKey); + request.setRawHeader("X-Sonos-Api-Key", m_clientKey); request.setUrl(QUrl(m_baseControlUrl + "/groups/" + groupId + "/playback/seekRelative")); QUuid actionId = QUuid::createUuid(); @@ -519,7 +551,7 @@ 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_apiKey); + request.setRawHeader("X-Sonos-Api-Key", m_clientKey); request.setUrl(QUrl(m_baseControlUrl + "/groups/" + groupId + "/playback/playMode")); QUuid actionId = QUuid::createUuid(); @@ -554,7 +586,7 @@ 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_apiKey); + request.setRawHeader("X-Sonos-Api-Key", m_clientKey); request.setUrl(QUrl(m_baseControlUrl + "/groups/" + groupId + "/playback/playMode")); QUuid actionId = QUuid::createUuid(); @@ -586,7 +618,7 @@ 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_apiKey); + request.setRawHeader("X-Sonos-Api-Key", m_clientKey); request.setUrl(QUrl(m_baseControlUrl + "/groups/" + groupId + "/playback/playMode")); QUuid actionId = QUuid::createUuid(); @@ -630,7 +662,7 @@ 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_apiKey); + request.setRawHeader("X-Sonos-Api-Key", m_clientKey); request.setUrl(QUrl(m_baseControlUrl + "/groups/" + groupId + "/playback/playMode")); QUuid actionId = QUuid::createUuid(); @@ -662,7 +694,7 @@ 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_apiKey); + request.setRawHeader("X-Sonos-Api-Key", m_clientKey); request.setUrl(QUrl(m_baseControlUrl + "/groups/" + groupId + "/playback/skipToNextTrack")); QUuid actionId = QUuid::createUuid(); @@ -688,7 +720,7 @@ 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_apiKey); + request.setRawHeader("X-Sonos-Api-Key", m_clientKey); request.setUrl(QUrl(m_baseControlUrl + "/groups/" + groupId + "/playback/skipToPreviousTrack")); QUuid actionId = QUuid::createUuid(); @@ -714,7 +746,7 @@ 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_apiKey); + request.setRawHeader("X-Sonos-Api-Key", m_clientKey); request.setUrl(QUrl(m_baseControlUrl + "/groups/" + groupId + "/playback/togglePlayPause")); QUuid actionId = QUuid::createUuid(); @@ -740,7 +772,7 @@ 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_apiKey); + 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] { @@ -871,7 +903,7 @@ 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_apiKey); + 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] { @@ -902,7 +934,7 @@ 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_apiKey); + request.setRawHeader("X-Sonos-Api-Key", m_clientKey); request.setUrl(QUrl(m_baseControlUrl + "/players/" + playerId + "/playerVolume")); QUuid actionId = QUuid::createUuid(); @@ -934,7 +966,7 @@ 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_apiKey); + request.setRawHeader("X-Sonos-Api-Key", m_clientKey); request.setUrl(QUrl(m_baseControlUrl + "/players/" + playerId + "/playerVolume/relative")); QUuid actionId = QUuid::createUuid(); @@ -964,7 +996,7 @@ 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_apiKey); + request.setRawHeader("X-Sonos-Api-Key", m_clientKey); request.setUrl(QUrl(m_baseControlUrl + "/players/" + playerId + "/playerVolume")); QUuid actionId = QUuid::createUuid(); @@ -994,7 +1026,7 @@ 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_apiKey); + request.setRawHeader("X-Sonos-Api-Key", m_clientKey); request.setUrl(QUrl(m_baseControlUrl + "/households/" + householdId + "/playlists/getPlaylist")); @@ -1041,7 +1073,7 @@ 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_apiKey); + 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] { @@ -1083,7 +1115,7 @@ 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_apiKey); + request.setRawHeader("X-Sonos-Api-Key", m_clientKey); request.setUrl(QUrl(m_baseControlUrl + "/groups/" + groupId + "/playlists")); QUuid actionId = QUuid::createUuid(); @@ -1112,7 +1144,7 @@ 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_apiKey); + 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] { @@ -1142,7 +1174,7 @@ QUuid Sonos::setPlayerSettings(const QString &playerId, PlayerSettingsObject set QNetworkRequest request; request.setHeader(QNetworkRequest::KnownHeaders::ContentTypeHeader, "application/json"); request.setRawHeader("Authorization", "Bearer " + m_accessToken); - request.setRawHeader("X-Sonos-Api-Key", m_apiKey); + request.setRawHeader("X-Sonos-Api-Key", m_clientKey); request.setUrl(QUrl(m_baseControlUrl + "/players/" + playerId + "/settings/player")); QUuid actionId = QUuid::createUuid(); @@ -1169,3 +1201,91 @@ QUuid Sonos::setPlayerSettings(const QString &playerId, PlayerSettingsObject set }); return actionId; } + +void Sonos::onRefreshTimeout() +{ + qCDebug(dcSonos) << "Refresh authentication token"; + getAccessTokenFromRefreshToken(m_refreshToken); +} + + +void Sonos::getAccessTokenFromRefreshToken(const QByteArray &refreshToken) +{ + QUrlQuery query; + query.addQueryItem("grant_type", "refresh_token"); + query.addQueryItem("refresh_token", refreshToken); + + QUrl url("https://api.sonos.com/login/v3/oauth"); + QNetworkRequest request(url); + request.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded; charset=UTF-8"); + QNetworkReply *reply = m_networkManager->post(request, QByteArray()); + connect(reply, &QNetworkReply::finished, this, [this, reply](){ + reply->deleteLater(); + + QJsonDocument jsonDoc = QJsonDocument::fromJson(reply->readAll()); + qCDebug(dcSonos()) << "Sonos accessToken reply:" << this << reply->error() << reply->errorString() << jsonDoc.toJson(); + if(!jsonDoc.toVariant().toMap().contains("access_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(); + + 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"; + return; + } + m_tokenRefreshTimer->start((expireTime - 20) * 1000); + } + emit authenticationStatusChanged(true);; + }); +} + +void Sonos::getAccessTokenFromAuthorizationCode(const QByteArray &authorizationCode) +{ + // Obtaining access token + QUrl url = QUrl(m_baseAuthorizationUrl); + QUrlQuery query; + query.clear(); + query.addQueryItem("grant_type", "authorization_code"); + query.addQueryItem("code", authorizationCode); + query.addQueryItem("redirect_uri", "https%3A%2F%2F127.0.0.1%3A8888"); + 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()); + 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"; + return; + } + m_tokenRefreshTimer->start((expireTime - 20) * 1000); + } + emit authenticationStatusChanged(true);; + }); +} diff --git a/sonos/sonos.h b/sonos/sonos.h index 4ce327a5..906784f3 100644 --- a/sonos/sonos.h +++ b/sonos/sonos.h @@ -24,6 +24,7 @@ #define SONOS_H #include +#include #include "network/networkaccessmanager.h" #include "devices/device.h" @@ -150,14 +151,14 @@ public: }; struct PlayBackObject { - QString itemId; - bool isDucking; - PlayBackState playbackState; - PlayMode playMode; - uint positionMillis; - QString previousItemId; - uint previousPositionMillis; - QString queueVersion; + QString itemId; + bool isDucking; + PlayBackState playbackState; + PlayMode playMode; + uint positionMillis; + QString previousItemId; + uint previousPositionMillis; + QString queueVersion; }; /* @@ -206,9 +207,13 @@ public: QList tracks; }; - explicit Sonos(NetworkAccessManager *networkManager, const QByteArray &accessToken, QObject *parent = nullptr); + explicit Sonos(NetworkAccessManager *networkManager, const QByteArray &clientId, const QByteArray &clientSecret, const QByteArray &refreshToken = "", QObject *parent = nullptr); - void setAccessToken(const QByteArray &accessToken); + QUrl getLoginUrl(const QUrl &redirectUrl); + QByteArray accessToken(); + QByteArray refreshToken(); + void getAccessTokenFromRefreshToken(const QByteArray &refreshToken); + void getAccessTokenFromAuthorizationCode(const QByteArray &authorizationCode); void getHouseholds(); void getFavorites(const QString &householdId); @@ -259,21 +264,27 @@ public: QUuid setPlayerSettings(const QString &playerId, PlayerSettingsObject settings); private: - QByteArray m_baseAuthorizationUrl = "https://api.sonos.com/login/v3/oauth"; + QByteArray m_baseAuthorizationUrl = "https://api.sonos.com/login/v3/oauth/access"; QByteArray m_baseControlUrl = "https://api.ws.sonos.com/control/api/v1"; - QByteArray m_apiKey = "b15cbf8c-a39c-47aa-bd93-635a96e9696c"; + QByteArray m_clientKey; + QByteArray m_clientSecret; + QByteArray m_accessToken; + QByteArray m_refreshToken; 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 favouritesReceived(const QString &householdId, QList favourites); void playlistsReceived(const QString &householdId, QList playlists); - void groupsReceived(QList groups); + void groupsReceived(const QString &householdId, QList groups); void playlistSummaryReceived(const QString &householdId, PlaylistSummaryObject playlistSummary); void playBackStatusReceived(const QString &groupId, PlayBackObject playBack); @@ -283,7 +294,5 @@ signals: 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 index a107f02e..ee83ccea 100644 --- a/sonos/sonos.pro +++ b/sonos/sonos.pro @@ -11,5 +11,3 @@ SOURCES += \ HEADERS += \ devicepluginsonos.h \ sonos.h \ - - From c8491bd345bf454d6131559769dece679892b335 Mon Sep 17 00:00:00 2001 From: nymea Date: Tue, 10 Sep 2019 23:09:48 +0200 Subject: [PATCH 13/22] fixed authentication --- sonos/devicepluginsonos.cpp | 87 +++++--- sonos/devicepluginsonos.json | 11 +- sonos/sonos.cpp | 418 ++++++++++++++++++++++++++++++----- sonos/sonos.h | 3 +- 4 files changed, 427 insertions(+), 92 deletions(-) diff --git a/sonos/devicepluginsonos.cpp b/sonos/devicepluginsonos.cpp index 02bb5be6..80bfce5d 100644 --- a/sonos/devicepluginsonos.cpp +++ b/sonos/devicepluginsonos.cpp @@ -37,8 +37,10 @@ DevicePluginSonos::DevicePluginSonos() DevicePluginSonos::~DevicePluginSonos() { - hardwareManager()->pluginTimerManager()->unregisterTimer(m_pluginTimer5sec); - hardwareManager()->pluginTimerManager()->unregisterTimer(m_pluginTimer60sec); + if (m_pluginTimer5sec) + hardwareManager()->pluginTimerManager()->unregisterTimer(m_pluginTimer5sec); + if (m_pluginTimer60sec) + hardwareManager()->pluginTimerManager()->unregisterTimer(m_pluginTimer60sec); } @@ -83,42 +85,54 @@ Device::DeviceSetupStatus DevicePluginSonos::setupDevice(Device *device) } if (device->deviceClassId() == sonosConnectionDeviceClassId) { - qCDebug(dcSonos()) << "Sonos OAuth setup complete"; 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); + m_sonosConnections.insert(device, sonos); + return Device::DeviceSetupStatusSuccess; } 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(); - sonos = new Sonos(hardwareManager()->networkManager(), "b15cbf8c-a39c-47aa-bd93-635a96e9696c", "c086ba71-e562-430b-a52f-867c6482fd11", "", this); + 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); sonos->getAccessTokenFromRefreshToken(refreshToken); + m_sonosConnections.insert(device, sonos); + return Device::DeviceSetupStatusAsync; } - - 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); - m_sonosConnections.insert(device, sonos); } if (device->deviceClassId() == sonosGroupDeviceClassId) { + return Device::DeviceSetupStatusSuccess; } - return Device::DeviceSetupStatusSuccess; + return Device::DeviceSetupStatusFailure; } DevicePairingInfo DevicePluginSonos::pairDevice(DevicePairingInfo &devicePairingInfo) { if (devicePairingInfo.deviceClassId() == sonosConnectionDeviceClassId) { - Sonos *sonos = new Sonos(hardwareManager()->networkManager(), "b15cbf8c-a39c-47aa-bd93-635a96e9696c", "c086ba71-e562-430b-a52f-867c6482fd11", "", this); - QUrl url = sonos->getLoginUrl(QUrl("https://127.0.0.1:8000")); + 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; devicePairingInfo.setOAuthUrl(url); devicePairingInfo.setStatus(Device::DeviceErrorNoError); @@ -134,28 +148,30 @@ DevicePairingInfo DevicePluginSonos::pairDevice(DevicePairingInfo &devicePairing DevicePairingInfo DevicePluginSonos::confirmPairing(DevicePairingInfo &devicePairingInfo, const QString &username, const QString &secret) { Q_UNUSED(username); - qCDebug(dcSonos()) << "Confirm pairing"; if (devicePairingInfo.deviceClassId() == sonosConnectionDeviceClassId) { - qCDebug(dcSonos()) << "Secret is" << secret; + qCDebug(dcSonos()) << "Redirect url is" << secret; QUrl url(secret); QUrlQuery query(url); - QByteArray accessCode = query.queryItemValue("code").toLocal8Bit(); - qCDebug(dcSonos()) << "Acess code is:" << accessCode; + 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(devicePairingInfo.deviceId()); if (!sonos) { + qWarning(dcSonos()) << "No sonos connection found for device:" << devicePairingInfo.deviceName(); m_setupSonosConnections.remove(devicePairingInfo.deviceId()); sonos->deleteLater(); devicePairingInfo.setStatus(Device::DeviceErrorHardwareFailure); return devicePairingInfo; } - sonos->getAccessTokenFromAuthorizationCode(accessCode); + sonos->getAccessTokenFromAuthorizationCode(authorizationCode); connect(sonos, &Sonos::authenticationStatusChanged, this, [devicePairingInfo, this](bool authenticated){ Sonos *sonos = static_cast(sender()); DevicePairingInfo info(devicePairingInfo); if(!authenticated) { + qWarning(dcSonos()) << "Authentication process failed" << devicePairingInfo.deviceName(); m_setupSonosConnections.remove(info.deviceId()); sonos->deleteLater(); info.setStatus(Device::DeviceErrorSetupFailed); @@ -164,9 +180,9 @@ DevicePairingInfo DevicePluginSonos::confirmPairing(DevicePairingInfo &devicePai } QByteArray accessToken = sonos->accessToken(); QByteArray refreshToken = sonos->refreshToken(); + qCDebug(dcSonos()) << "Token:" << accessToken << refreshToken; pluginStorage()->beginGroup(info.deviceId().toString()); - pluginStorage()->setValue("access_token", accessToken); pluginStorage()->setValue("refresh_token", refreshToken); pluginStorage()->endGroup(); @@ -311,6 +327,8 @@ 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())) { @@ -322,13 +340,24 @@ void DevicePluginSonos::onAuthenticationStatusChanged(bool authenticated) { Sonos *sonosConnection = static_cast(sender()); Device *device = m_sonosConnections.key(sonosConnection); - 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); + if (!device) + return; + + if (!device->setupComplete()) { + if (authenticated) { + emit deviceSetupFinished(device, Device::DeviceSetupStatusSuccess); + } else { + emit deviceSetupFinished(device, Device::DeviceSetupStatusFailure); + } + } else { + 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); + } } } diff --git a/sonos/devicepluginsonos.json b/sonos/devicepluginsonos.json index cfbc056d..aa46d38b 100644 --- a/sonos/devicepluginsonos.json +++ b/sonos/devicepluginsonos.json @@ -24,7 +24,7 @@ "name": "connected", "displayName": "connected", "displayNameEvent": "connected changed", - "defaultValue": false, + "defaultValue": true, "type": "bool" }, { @@ -32,8 +32,15 @@ "name": "loggedIn", "displayName": "Logged in", "displayNameEvent": "Logged in changed", - "defaultValue": false, + "defaultValue": true, "type": "bool" + }, + { + "id": "fb993eab-f1b5-44dd-9b99-041faec5a3b9", + "name": "userDisplayName", + "displayName": "User name", + "displayNameEvent": "User name changed", + "type": "QString" } ] }, diff --git a/sonos/sonos.cpp b/sonos/sonos.cpp index 6fc27dba..1f58218e 100644 --- a/sonos/sonos.cpp +++ b/sonos/sonos.cpp @@ -28,11 +28,10 @@ #include #include -Sonos::Sonos(NetworkAccessManager *networkmanager, const QByteArray &clientKey, const QByteArray &clientSecret, const QByteArray &refreshToken, QObject *parent) : +Sonos::Sonos(NetworkAccessManager *networkmanager, const QByteArray &clientKey, const QByteArray &clientSecret, QObject *parent) : QObject(parent), m_clientKey(clientKey), m_clientSecret(clientSecret), - m_refreshToken(refreshToken), m_networkManager(networkmanager) { if(!m_tokenRefreshTimer) { @@ -44,12 +43,20 @@ Sonos::Sonos(NetworkAccessManager *networkmanager, const QByteArray &clientKey, QUrl Sonos::getLoginUrl(const QUrl &redirectUrl) { - QString clientId = "b15cbf8c-a39c-47aa-bd93-635a96e9696c"; + 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", clientId); - queryParams.addQueryItem("redirect_uri", redirectUrl.toString()); + 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()); @@ -81,20 +88,20 @@ void Sonos::getHouseholds() reply->deleteLater(); int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); - connectionChanged(true); - - if (status == 401) { - //Authentication required - getAccessTokenFromRefreshToken(m_refreshToken); - return; - } // 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(); QJsonDocument data = QJsonDocument::fromJson(reply->readAll()); if (!data.isObject()) { qDebug(dcSonos()) << "Household ID: Recieved invalide JSON object"; @@ -132,10 +139,18 @@ QUuid Sonos::loadFavorite(const QString &groupId, const QString &favouriteId) // Check HTTP status code if (status != 200 || reply->error() != QNetworkReply::NoError) { - qCWarning(dcSonos()) << "Request error:" << status << reply->errorString(); + 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; @@ -155,9 +170,17 @@ void Sonos::getFavorites(const QString &householdId) // 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()) @@ -197,9 +220,17 @@ void Sonos::getGroups(const QString &householdId) // 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(); QJsonDocument data = QJsonDocument::fromJson(reply->readAll()); @@ -237,9 +268,17 @@ void Sonos::getGroupVolume(const QString &groupId) // 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(); QJsonDocument data = QJsonDocument::fromJson(reply->readAll()); @@ -277,12 +316,20 @@ QUuid Sonos::setGroupVolume(const QString &groupId, int volume) // Check HTTP status code if (status != 200 || reply->error() != QNetworkReply::NoError) { - qCWarning(dcSonos()) << "Request error:" << status << reply->errorString(); + 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; } - getGroupVolume(groupId); + emit connectionChanged(true); + emit authenticationStatusChanged(true); emit actionExecuted(actionId, true); + getGroupVolume(groupId); }); return actionId; } @@ -309,12 +356,20 @@ QUuid Sonos::setGroupMute(const QString &groupId, bool mute) // Check HTTP status code if (status != 200 || reply->error() != QNetworkReply::NoError) { - qCWarning(dcSonos()) << "Request error:" << status << reply->errorString(); + 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; } - getGroupVolume(groupId); + emit connectionChanged(true); + emit authenticationStatusChanged(true); emit actionExecuted(actionId, true); + getGroupVolume(groupId); }); return actionId; } @@ -341,12 +396,20 @@ QUuid Sonos::setGroupRelativeVolume(const QString &groupId, int volumeDelta) // Check HTTP status code if (status != 200 || reply->error() != QNetworkReply::NoError) { - qCWarning(dcSonos()) << "Request error:" << status << reply->errorString(); + 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; } - getGroupVolume(groupId); + emit connectionChanged(true); + emit authenticationStatusChanged(true); emit actionExecuted(actionId, true); + getGroupVolume(groupId); }); return actionId; } @@ -365,9 +428,17 @@ void Sonos::getGroupPlaybackStatus(const QString &groupId) // 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()) @@ -421,12 +492,20 @@ QUuid Sonos::groupLoadLineIn(const QString &groupId) // Check HTTP status code if (status != 200 || reply->error() != QNetworkReply::NoError) { - qCWarning(dcSonos()) << "Request error:" << status << reply->errorString(); + 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; } - getGroupVolume(groupId); + emit connectionChanged(true); + emit authenticationStatusChanged(true); emit actionExecuted(actionId, true); + getGroupVolume(groupId); }); return actionId; } @@ -449,12 +528,20 @@ QUuid Sonos::groupPlay(const QString &groupId) // Check HTTP status code if (status != 200 || reply->error() != QNetworkReply::NoError) { - qCWarning(dcSonos()) << "Request error:" << status << reply->errorString(); + 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; } - getGroupPlaybackStatus(groupId); + emit connectionChanged(true); + emit authenticationStatusChanged(true); emit actionExecuted(actionId, true); + getGroupPlaybackStatus(groupId); }); return actionId; } @@ -477,12 +564,20 @@ QUuid Sonos::groupPause(const QString &groupId) // Check HTTP status code if (status != 200 || reply->error() != QNetworkReply::NoError) { - qCWarning(dcSonos()) << "Request error:" << status << reply->errorString(); + 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; } - getGroupPlaybackStatus(groupId); + emit connectionChanged(true); + emit authenticationStatusChanged(true); emit actionExecuted(actionId, true); + getGroupPlaybackStatus(groupId); }); return actionId; } @@ -508,10 +603,18 @@ QUuid Sonos::groupSeek(const QString &groupId, int possitionMillis) // Check HTTP status code if (status != 200 || reply->error() != QNetworkReply::NoError) { - qCWarning(dcSonos()) << "Request error:" << status << reply->errorString(); + 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; @@ -537,10 +640,18 @@ QUuid Sonos::groupSeekRelative(const QString &groupId, int deltaMillis) // Check HTTP status code if (status != 200 || reply->error() != QNetworkReply::NoError) { - qCWarning(dcSonos()) << "Request error:" << status << reply->errorString(); + 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; @@ -571,12 +682,20 @@ QUuid Sonos::groupSetPlayModes(const QString &groupId, PlayMode playMode) // Check HTTP status code if (status != 200 || reply->error() != QNetworkReply::NoError) { - qCWarning(dcSonos()) << "Request error:" << status << reply->errorString(); + 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; } - getGroupPlaybackStatus(groupId); + emit connectionChanged(true); + emit authenticationStatusChanged(true); emit actionExecuted(actionId, true); + getGroupPlaybackStatus(groupId); }); return actionId; } @@ -603,12 +722,20 @@ QUuid Sonos::groupSetShuffle(const QString &groupId, bool shuffle) // Check HTTP status code if (status != 200 || reply->error() != QNetworkReply::NoError) { - qCWarning(dcSonos()) << "Request error:" << status << reply->errorString(); + 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; } - getGroupPlaybackStatus(groupId); + emit connectionChanged(true); + emit authenticationStatusChanged(true); emit actionExecuted(actionId, true); + getGroupPlaybackStatus(groupId); }); return actionId; } @@ -647,12 +774,20 @@ QUuid Sonos::groupSetRepeat(const QString &groupId, RepeatMode repeatMode) // Check HTTP status code if (status != 200 || reply->error() != QNetworkReply::NoError) { - qCWarning(dcSonos()) << "Request error:" << status << reply->errorString(); + 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; } - getGroupPlaybackStatus(groupId); + emit connectionChanged(true); + emit authenticationStatusChanged(true); emit actionExecuted(actionId, true); + getGroupPlaybackStatus(groupId); }); return actionId; } @@ -679,12 +814,20 @@ QUuid Sonos::groupSetCrossfade(const QString &groupId, bool crossfade) // Check HTTP status code if (status != 200 || reply->error() != QNetworkReply::NoError) { - qCWarning(dcSonos()) << "Request error:" << status << reply->errorString(); + 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; } - getGroupPlaybackStatus(groupId); + emit connectionChanged(true); + emit authenticationStatusChanged(true); emit actionExecuted(actionId, true); + getGroupPlaybackStatus(groupId); }); return actionId; } @@ -705,12 +848,20 @@ QUuid Sonos::groupSkipToNextTrack(const QString &groupId) // 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(); - actionExecuted(actionId, false); return; } + emit connectionChanged(true); + emit authenticationStatusChanged(true); + emit actionExecuted(actionId, true); getGroupMetadataStatus(groupId); - actionExecuted(actionId, true); }); return actionId; } @@ -731,12 +882,20 @@ QUuid Sonos::groupSkipToPreviousTrack(const QString &groupId) // 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(); - actionExecuted(actionId, false); return; } + emit connectionChanged(true); + emit authenticationStatusChanged(true); + emit actionExecuted(actionId, true); getGroupMetadataStatus(groupId); - actionExecuted(actionId, true); }); return actionId; } @@ -757,12 +916,20 @@ QUuid Sonos::groupTogglePlayPause(const QString &groupId) // 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(); - actionExecuted(actionId, false); return; } + emit connectionChanged(true); + emit authenticationStatusChanged(true); + emit actionExecuted(actionId, true); getGroupPlaybackStatus(groupId); - actionExecuted(actionId, true); }); return actionId; } @@ -781,9 +948,17 @@ void Sonos::getGroupMetadataStatus(const QString &groupId) // 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()) @@ -912,9 +1087,17 @@ void Sonos::getPlayerVolume(const QByteArray &playerId) // 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(); QJsonDocument data = QJsonDocument::fromJson(reply->readAll()); @@ -951,12 +1134,20 @@ QUuid Sonos::setPlayerVolume(const QByteArray &playerId, int volume) // Check HTTP status code if (status != 200 || reply->error() != QNetworkReply::NoError) { - qCWarning(dcSonos()) << "Request error:" << status << reply->errorString(); + 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; } - getPlayerVolume(playerId); + emit connectionChanged(true); + emit authenticationStatusChanged(true); emit actionExecuted(actionId, true); + getPlayerVolume(playerId); }); return actionId; } @@ -981,12 +1172,20 @@ QUuid Sonos::setPlayerRelativeVolume(const QByteArray &playerId, int volumeDelta // Check HTTP status code if (status != 200 || reply->error() != QNetworkReply::NoError) { - qCWarning(dcSonos()) << "Request error:" << status << reply->errorString(); + 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; } - getPlayerVolume(playerId); + emit connectionChanged(true); + emit authenticationStatusChanged(true); emit actionExecuted(actionId, true); + getPlayerVolume(playerId); }); return actionId; } @@ -1011,12 +1210,20 @@ QUuid Sonos::setPlayerMute(const QByteArray &playerId, bool mute) // Check HTTP status code if (status != 200 || reply->error() != QNetworkReply::NoError) { - qCWarning(dcSonos()) << "Request error:" << status << reply->errorString(); + 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; } - getPlayerVolume(playerId); + emit connectionChanged(true); + emit authenticationStatusChanged(true); emit actionExecuted(actionId, true); + getPlayerVolume(playerId); }); return actionId; } @@ -1041,9 +1248,17 @@ void Sonos::getPlaylist(const QString &householdId, const QString &playlistId) // 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(); QJsonDocument data = QJsonDocument::fromJson(reply->readAll()); @@ -1082,9 +1297,17 @@ void Sonos::getPlaylists(const QString &householdId) // 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(); QJsonDocument data = QJsonDocument::fromJson(reply->readAll()); @@ -1130,10 +1353,18 @@ QUuid Sonos::loadPlaylist(const QString &groupId, const QString &playlistId) // Check HTTP status code if (status != 200 || reply->error() != QNetworkReply::NoError) { - qCWarning(dcSonos()) << "Request error:" << status << reply->errorString(); + 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; @@ -1153,9 +1384,18 @@ void Sonos::getPlayerSettings(const QString &playerId) // 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; @@ -1192,12 +1432,20 @@ QUuid Sonos::setPlayerSettings(const QString &playerId, PlayerSettingsObject set // Check HTTP status code if (status != 200 || reply->error() != QNetworkReply::NoError) { - qCWarning(dcSonos()) << "Request error:" << status << reply->errorString(); + 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; } - getPlayerSettings(playerId); + emit connectionChanged(true); + emit authenticationStatusChanged(true); emit actionExecuted(actionId, true); + getPlayerSettings(playerId); }); return actionId; } @@ -1211,31 +1459,49 @@ void Sonos::onRefreshTimeout() 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); - QUrl url("https://api.sonos.com/login/v3/oauth"); 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()); - qCDebug(dcSonos()) << "Sonos accessToken reply:" << this << reply->error() << reply->errorString() << jsonDoc.toJson(); + 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; } - qCDebug(dcSonos()) << "Access token:" << jsonDoc.toVariant().toMap().value("access_token").toString(); 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()) << "expires at" << QDateTime::currentDateTime().addSecs(expireTime).toString(); + qCDebug(dcSonos()) << "Access token expires at" << QDateTime::currentDateTime().addSecs(expireTime).toString(); if (!m_tokenRefreshTimer) { - qWarning(dcSonos()) << "Token refresh timer not initialized"; + qWarning(dcSonos()) << "Access token refresh timer not initialized"; return; } m_tokenRefreshTimer->start((expireTime - 20) * 1000); @@ -1247,12 +1513,19 @@ void Sonos::getAccessTokenFromRefreshToken(const QByteArray &refreshToken) 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", "https%3A%2F%2F127.0.0.1%3A8888"); + query.addQueryItem("redirect_uri", m_redirectUri); url.setQuery(query); QNetworkRequest request(url); @@ -1264,11 +1537,35 @@ void Sonos::getAccessTokenFromAuthorizationCode(const QByteArray &authorizationC 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);; + emit authenticationStatusChanged(false); return; } qCDebug(dcSonos()) << "Access token:" << jsonDoc.toVariant().toMap().value("access_token").toString(); @@ -1282,10 +1579,11 @@ void Sonos::getAccessTokenFromAuthorizationCode(const QByteArray &authorizationC 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);; + emit authenticationStatusChanged(true); }); } diff --git a/sonos/sonos.h b/sonos/sonos.h index 906784f3..8d6d7cc7 100644 --- a/sonos/sonos.h +++ b/sonos/sonos.h @@ -207,7 +207,7 @@ public: QList tracks; }; - explicit Sonos(NetworkAccessManager *networkManager, const QByteArray &clientId, const QByteArray &clientSecret, const QByteArray &refreshToken = "", QObject *parent = nullptr); + explicit Sonos(NetworkAccessManager *networkManager, const QByteArray &clientId, const QByteArray &clientSecret, QObject *parent = nullptr); QUrl getLoginUrl(const QUrl &redirectUrl); QByteArray accessToken(); @@ -271,6 +271,7 @@ private: QByteArray m_accessToken; QByteArray m_refreshToken; + QByteArray m_redirectUri; NetworkAccessManager *m_networkManager = nullptr; QTimer *m_tokenRefreshTimer = nullptr; From 23ff818577281dcdb3fc718c1d70cf79c0cc12f1 Mon Sep 17 00:00:00 2001 From: Michael Zanetti Date: Wed, 9 Oct 2019 14:52:58 +0200 Subject: [PATCH 14/22] Update to latest api changes --- sonos/devicepluginsonos.cpp | 169 ++++++++++++++++++----------------- sonos/devicepluginsonos.h | 10 +-- sonos/devicepluginsonos.json | 5 +- sonos/sonos.cpp | 114 +++++++++++++---------- 4 files changed, 164 insertions(+), 134 deletions(-) diff --git a/sonos/devicepluginsonos.cpp b/sonos/devicepluginsonos.cpp index 80bfce5d..2fa4eb90 100644 --- a/sonos/devicepluginsonos.cpp +++ b/sonos/devicepluginsonos.cpp @@ -44,8 +44,10 @@ DevicePluginSonos::~DevicePluginSonos() } -Device::DeviceSetupStatus DevicePluginSonos::setupDevice(Device *device) +void DevicePluginSonos::setupDevice(DeviceSetupInfo *info) { + Device *device = info->device(); + if (!m_pluginTimer5sec) { m_pluginTimer5sec = hardwareManager()->pluginTimerManager()->registerTimer(5); connect(m_pluginTimer5sec, &PluginTimer::timeout, this, [this]() { @@ -98,8 +100,17 @@ Device::DeviceSetupStatus DevicePluginSonos::setupDevice(Device *device) 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 Device::DeviceSetupStatusSuccess; + return info->finish(Device::DeviceErrorNoError); } else { //device loaded from the device database, needs a new access token; pluginStorage()->beginGroup(device->id().toString()); @@ -117,39 +128,39 @@ Device::DeviceSetupStatus DevicePluginSonos::setupDevice(Device *device) connect(sonos, &Sonos::authenticationStatusChanged, this, &DevicePluginSonos::onAuthenticationStatusChanged); sonos->getAccessTokenFromRefreshToken(refreshToken); m_sonosConnections.insert(device, sonos); - return Device::DeviceSetupStatusAsync; + return info->finish(Device::DeviceErrorNoError); } } if (device->deviceClassId() == sonosGroupDeviceClassId) { - return Device::DeviceSetupStatusSuccess; + return info->finish(Device::DeviceErrorNoError); } - return Device::DeviceSetupStatusFailure; + + qCWarning(dcSonos()) << "Unhandled device class id in setupDevice" << device->deviceClassId(); } -DevicePairingInfo DevicePluginSonos::pairDevice(DevicePairingInfo &devicePairingInfo) +void DevicePluginSonos::startPairing(DevicePairingInfo *info) { - if (devicePairingInfo.deviceClassId() == sonosConnectionDeviceClassId) { + 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; - devicePairingInfo.setOAuthUrl(url); - devicePairingInfo.setStatus(Device::DeviceErrorNoError); - m_setupSonosConnections.insert(devicePairingInfo.deviceId(), sonos); - return devicePairingInfo; + info->setOAuthUrl(url); + info->finish(Device::DeviceErrorNoError); + m_setupSonosConnections.insert(info->deviceId(), sonos); + return; } qCWarning(dcSonos()) << "Unhandled pairing metod!"; - devicePairingInfo.setStatus(Device::DeviceErrorCreationMethodNotSupported); - return devicePairingInfo; + info->finish(Device::DeviceErrorCreationMethodNotSupported); } -DevicePairingInfo DevicePluginSonos::confirmPairing(DevicePairingInfo &devicePairingInfo, const QString &username, const QString &secret) +void DevicePluginSonos::confirmPairing(DevicePairingInfo *info, const QString &username, const QString &secret) { - Q_UNUSED(username); + Q_UNUSED(username) - if (devicePairingInfo.deviceClassId() == sonosConnectionDeviceClassId) { + if (info->deviceClassId() == sonosConnectionDeviceClassId) { qCDebug(dcSonos()) << "Redirect url is" << secret; QUrl url(secret); QUrlQuery query(url); @@ -157,44 +168,39 @@ DevicePairingInfo DevicePluginSonos::confirmPairing(DevicePairingInfo &devicePai QByteArray state = query.queryItemValue("state").toLocal8Bit(); //TODO evaluate state if it equals the given state - Sonos *sonos = m_setupSonosConnections.value(devicePairingInfo.deviceId()); + Sonos *sonos = m_setupSonosConnections.value(info->deviceId()); if (!sonos) { - qWarning(dcSonos()) << "No sonos connection found for device:" << devicePairingInfo.deviceName(); - m_setupSonosConnections.remove(devicePairingInfo.deviceId()); + qWarning(dcSonos()) << "No sonos connection found for device:" << info->deviceName(); + m_setupSonosConnections.remove(info->deviceId()); sonos->deleteLater(); - devicePairingInfo.setStatus(Device::DeviceErrorHardwareFailure); - return devicePairingInfo; + info->finish(Device::DeviceErrorHardwareFailure); + return; } sonos->getAccessTokenFromAuthorizationCode(authorizationCode); - connect(sonos, &Sonos::authenticationStatusChanged, this, [devicePairingInfo, this](bool authenticated){ + connect(sonos, &Sonos::authenticationStatusChanged, info, [this, info](bool authenticated){ Sonos *sonos = static_cast(sender()); - DevicePairingInfo info(devicePairingInfo); if(!authenticated) { - qWarning(dcSonos()) << "Authentication process failed" << devicePairingInfo.deviceName(); - m_setupSonosConnections.remove(info.deviceId()); + qWarning(dcSonos()) << "Authentication process failed" << info->deviceName(); + m_setupSonosConnections.remove(info->deviceId()); sonos->deleteLater(); - info.setStatus(Device::DeviceErrorSetupFailed); - emit pairingFinished(info); + 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()->beginGroup(info->deviceId().toString()); pluginStorage()->setValue("refresh_token", refreshToken); pluginStorage()->endGroup(); - info.setStatus(Device::DeviceErrorNoError); - emit pairingFinished(info); + info->finish(Device::DeviceErrorNoError); }); - devicePairingInfo.setStatus(Device::DeviceErrorAsync); - return devicePairingInfo; + return; } qCWarning(dcSonos()) << "Invalid deviceclassId -> no pairing possible with this device"; - devicePairingInfo.setStatus(Device::DeviceErrorHardwareFailure); - return devicePairingInfo; + info->finish(Device::DeviceErrorDeviceClassNotFound); } void DevicePluginSonos::postSetupDevice(Device *device) @@ -239,88 +245,91 @@ void DevicePluginSonos::deviceRemoved(Device *device) } -Device::DeviceError DevicePluginSonos::executeAction(Device *device, const Action &action) +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 Device::DeviceErrorInvalidParameter; + return info->finish(Device::DeviceErrorHardwareNotAvailable, QT_TR_NOOP("Sonos device is not available.")); } if (action.actionTypeId() == sonosGroupPlayActionTypeId) { - m_pendingActions.insert(sonos->groupPlay(groupId), action.id()); - return Device::DeviceErrorAsync; + 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), action.id()); - return Device::DeviceErrorAsync; + 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), action.id()); + 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), action.id()); + 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), action.id()); + m_pendingActions.insert(sonos->groupSetRepeat(groupId, Sonos::RepeatModeAll), QPointer(info)); } else { - return Device::DeviceErrorHardwareFailure; + return info->finish(Device::DeviceErrorHardwareFailure); } - return Device::DeviceErrorAsync; + return; } if (action.actionTypeId() == sonosGroupPauseActionTypeId) { - m_pendingActions.insert(sonos->groupPause(groupId), action.id()); - return Device::DeviceErrorAsync; + m_pendingActions.insert(sonos->groupPause(groupId), QPointer(info)); + return; } if (action.actionTypeId() == sonosGroupStopActionTypeId) { - m_pendingActions.insert(sonos->groupPause(groupId), action.id()); - return Device::DeviceErrorAsync; + 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), action.id()); - return Device::DeviceErrorAsync; + 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), action.id()); - return Device::DeviceErrorAsync; + m_pendingActions.insert(sonos->setGroupVolume(groupId, volume), QPointer(info)); + return; } if (action.actionTypeId() == sonosGroupSkipNextActionTypeId) { - m_pendingActions.insert(sonos->groupSkipToNextTrack(groupId), action.id()); - return Device::DeviceErrorAsync; + m_pendingActions.insert(sonos->groupSkipToNextTrack(groupId), QPointer(info)); + return; } if (action.actionTypeId() == sonosGroupSkipBackActionTypeId) { - m_pendingActions.insert(sonos->groupSkipToPreviousTrack(groupId), action.id()); - return Device::DeviceErrorAsync; + 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), action.id()); + m_pendingActions.insert(sonos->groupPlay(groupId), QPointer(info)); } else if(playbackStatus == "Stopped") { - m_pendingActions.insert(sonos->groupPause(groupId), action.id()); + m_pendingActions.insert(sonos->groupPause(groupId), QPointer(info)); } else if(playbackStatus == "Paused") { - m_pendingActions.insert(sonos->groupPause(groupId), action.id()); + m_pendingActions.insert(sonos->groupPause(groupId), QPointer(info)); } - return Device::DeviceErrorAsync; + return; } - return Device::DeviceErrorActionTypeNotFound; + return info->finish(Device::DeviceErrorActionTypeNotFound); } - return Device::DeviceErrorDeviceClassNotFound; + info->finish(Device::DeviceErrorDeviceClassNotFound); } void DevicePluginSonos::onConnectionChanged(bool connected) @@ -343,21 +352,13 @@ void DevicePluginSonos::onAuthenticationStatusChanged(bool authenticated) if (!device) return; - if (!device->setupComplete()) { - if (authenticated) { - emit deviceSetupFinished(device, Device::DeviceSetupStatusSuccess); - } else { - emit deviceSetupFinished(device, Device::DeviceSetupStatusFailure); - } - } else { - 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); - } + 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); } } @@ -421,7 +422,7 @@ void DevicePluginSonos::onGroupsReceived(const QString &householdId, QListid())) { @@ -495,11 +496,15 @@ void DevicePluginSonos::onVolumeReceived(const QString &groupId, Sonos::VolumeOb void DevicePluginSonos::onActionExecuted(QUuid sonosActionId, bool success) { if (m_pendingActions.contains(sonosActionId)) { - ActionId nymeaActionId = m_pendingActions.value(sonosActionId); + QPointer info = m_pendingActions.value(sonosActionId); + if (info.isNull()) { + qCWarning(dcSonos()) << "DeviceActionInfo has disappeared. Did it time out?"; + return; + } if (success) { - emit actionExecutionFinished(nymeaActionId, Device::DeviceErrorNoError); + info->finish(Device::DeviceErrorNoError); } else { - emit actionExecutionFinished(nymeaActionId, Device::DeviceErrorHardwareFailure); + info->finish(Device::DeviceErrorHardwareFailure); } } } diff --git a/sonos/devicepluginsonos.h b/sonos/devicepluginsonos.h index fcf2eb38..f72d4796 100644 --- a/sonos/devicepluginsonos.h +++ b/sonos/devicepluginsonos.h @@ -40,14 +40,14 @@ public: explicit DevicePluginSonos(); ~DevicePluginSonos() override; - Device::DeviceSetupStatus setupDevice(Device *device) override; - DevicePairingInfo pairDevice(DevicePairingInfo &devicePairingInfo) override; - DevicePairingInfo confirmPairing(DevicePairingInfo &devicePairingInfo, const QString &username, const QString &secret) 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; - Device::DeviceError executeAction(Device *device, const Action &action) override; + void executeAction(DeviceActionInfo *info) override; private: PluginTimer *m_pluginTimer5sec = nullptr; @@ -60,7 +60,7 @@ private: QByteArray m_sonosConnectionAccessToken; QByteArray m_sonosConnectionRefreshToken; - QHash m_pendingActions; + QHash > m_pendingActions; private slots: void onConnectionChanged(bool connected); diff --git a/sonos/devicepluginsonos.json b/sonos/devicepluginsonos.json index aa46d38b..97b23875 100644 --- a/sonos/devicepluginsonos.json +++ b/sonos/devicepluginsonos.json @@ -13,7 +13,7 @@ "id": "22df416d-7732-44f1-b6b9-e41296211178", "name": "sonosConnection", "displayName": "Sonos connection", - "interfaces": ["gateway"], + "interfaces": ["account", "gateway"], "createMethods": ["user"], "setupMethod": "oauth", "paramTypes": [ @@ -40,7 +40,8 @@ "name": "userDisplayName", "displayName": "User name", "displayNameEvent": "User name changed", - "type": "QString" + "type": "QString", + "defaultValue": "" } ] }, diff --git a/sonos/sonos.cpp b/sonos/sonos.cpp index 1f58218e..356a0f80 100644 --- a/sonos/sonos.cpp +++ b/sonos/sonos.cpp @@ -102,15 +102,15 @@ void Sonos::getHouseholds() emit connectionChanged(true); emit authenticationStatusChanged(true); - QJsonDocument data = QJsonDocument::fromJson(reply->readAll()); - if (!data.isObject()) { + 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; - QJsonArray jsonArray = data["households"].toArray(); - foreach (const QJsonValue & value, jsonArray) { - QJsonObject obj = value.toObject(); + 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()); } @@ -182,18 +182,21 @@ void Sonos::getFavorites(const QString &householdId) emit connectionChanged(true); emit authenticationStatusChanged(true); - QJsonDocument data = QJsonDocument::fromJson(reply->readAll()); - if (!data.isObject()) + 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; - if (!data["items"].isArray()) - return; - - QJsonArray array = data["items"].toArray(); + QVariantList array = data.toVariant().toMap().value("items").toList(); QList favourites ; - foreach (const QJsonValue & value, array) { - QJsonObject itemObject = value.toObject(); + foreach (const QVariant &variant, array) { + QVariantMap itemObject = variant.toMap(); qDebug(dcSonos()) << "Item ID received:" << itemObject["id"].toString(); FavouriteObject favourite; favourite.id = itemObject["id"].toString(); @@ -233,17 +236,18 @@ void Sonos::getGroups(const QString &householdId) emit authenticationStatusChanged(true); //qDebug(dcSonos()) << "Received response from Sonos" << reply->readAll(); - QJsonDocument data = QJsonDocument::fromJson(reply->readAll()); - if (!data.isObject()) + QJsonParseError error; + QJsonDocument data = QJsonDocument::fromJson(reply->readAll(), &error); + if (error.error != QJsonParseError::NoError) return; - if (!data["groups"].isArray()) + if (!data.toVariant().toMap().contains("groups")) return; - QJsonArray array = data["groups"].toArray(); + QVariantList array = data.toVariant().toMap().value("groups").toList(); QList groupObjects; - foreach (const QJsonValue & value, array) { - QJsonObject obj = value.toObject(); + foreach (const QVariant &value, array) { + QVariantMap obj = value.toMap(); qDebug(dcSonos()) << "Group ID received:" << obj["id"].toString(); GroupObject group; group.groupId = obj["id"].toString(); @@ -281,15 +285,19 @@ void Sonos::getGroupVolume(const QString &groupId) emit authenticationStatusChanged(true); //qDebug(dcSonos()) << "Received response from Sonos" << reply->readAll(); - QJsonDocument data = QJsonDocument::fromJson(reply->readAll()); - if (!data.isObject()) + QJsonParseError error; + QJsonDocument data = QJsonDocument::fromJson(reply->readAll(), &error); + if (error.error != QJsonParseError::NoError) { + qCWarning(dcSonos()) << "JSON Parse error" << error.errorString(); return; + } VolumeObject volume; - volume.volume = data["volume"].toInt(); - volume.muted = data["muted"].toBool(); - volume.fixed = data["fixed"].toBool(); + QVariantMap variant = data.toVariant().toMap(); + volume.volume = variant["volume"].toInt(); + volume.muted = variant["muted"].toBool(); + volume.fixed = variant["fixed"].toBool(); emit volumeReceived(groupId, volume); }); @@ -1100,14 +1108,18 @@ void Sonos::getPlayerVolume(const QByteArray &playerId) emit authenticationStatusChanged(true); //qDebug(dcSonos()) << "Received response from Sonos" << reply->readAll(); - QJsonDocument data = QJsonDocument::fromJson(reply->readAll()); - if (!data.isObject()) + QJsonParseError error; + QJsonDocument data = QJsonDocument::fromJson(reply->readAll(), &error); + if (error.error != QJsonParseError::NoError) { + qCWarning(dcSonos()) << "Json parse error" << error.errorString(); return; + } VolumeObject volume; - volume.volume = data["volume"].toInt(); - volume.muted = data["muted"].toBool(); - volume.fixed = data["fixed"].toBool(); + QVariantMap variant = data.toVariant().toMap(); + volume.volume = variant["volume"].toInt(); + volume.muted = variant["muted"].toBool(); + volume.fixed = variant["fixed"].toBool(); emit playerVolumeReceived(playerId, volume); }); } @@ -1261,17 +1273,22 @@ void Sonos::getPlaylist(const QString &householdId, const QString &playlistId) emit authenticationStatusChanged(true); //qDebug(dcSonos()) << "Received response from Sonos" << reply->readAll(); - QJsonDocument data = QJsonDocument::fromJson(reply->readAll()); - if (!data.isObject()) + QJsonParseError error; + QJsonDocument data = QJsonDocument::fromJson(reply->readAll(), &error); + if (error.error != QJsonParseError::NoError) { + qCWarning(dcSonos()) << "Json parse error" << error.errorString(); return; + } - if (!data["tracks"].isArray()) + QVariantMap variant = data.toVariant().toMap(); + + if (!variant.contains("tracks")) return; PlaylistSummaryObject playlist; - QJsonArray array = data["tracks"].toArray(); - foreach (const QJsonValue & value, array) { - QJsonObject itemObject = value.toObject(); + QVariantList array = variant["tracks"].toList(); + foreach (const QVariant &value, array) { + QVariantMap itemObject = value.toMap(); qDebug(dcSonos()) << "Item ID received:" << itemObject["id"].toString(); PlaylistTrackObject track; track.name = itemObject["name"].toString(); @@ -1310,17 +1327,20 @@ void Sonos::getPlaylists(const QString &householdId) emit authenticationStatusChanged(true); //qDebug(dcSonos()) << "Received response from Sonos" << reply->readAll(); - QJsonDocument data = QJsonDocument::fromJson(reply->readAll()); - if (!data.isObject()) + 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; - if (!data["items"].isArray()) - return; - - QJsonArray array = data["playlists"].toArray(); + QVariantList array = data.toVariant().toMap().value("playlists").toList(); QList playlists; - foreach (const QJsonValue & value, array) { - QJsonObject itemObject = value.toObject(); + foreach (const QVariant &value, array) { + QVariantMap itemObject = value.toMap(); qDebug(dcSonos()) << "Item ID received:" << itemObject["id"].toString(); PlaylistObject playlist; playlist.id = itemObject["id"].toString(); @@ -1396,10 +1416,14 @@ void Sonos::getPlayerSettings(const QString &playerId) emit connectionChanged(true); emit authenticationStatusChanged(true); - QJsonDocument data = QJsonDocument::fromJson(reply->readAll()); - if (!data.isObject()) + 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(); From 51f7acdfc0ab24fcf2d12cc1ca1c0e56e4d2ead8 Mon Sep 17 00:00:00 2001 From: Michael Zanetti Date: Wed, 9 Oct 2019 16:17:17 +0200 Subject: [PATCH 15/22] Add install file for sonos plugin --- debian/nymea-plugin-sonos.install.in | 1 + 1 file changed, 1 insertion(+) create mode 100644 debian/nymea-plugin-sonos.install.in 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 From 313d562a308185a50d6bfa266af42a25aa19a8a4 Mon Sep 17 00:00:00 2001 From: Michael Zanetti Date: Wed, 9 Oct 2019 17:21:07 +0200 Subject: [PATCH 16/22] Add package --- debian/control | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/debian/control b/debian/control index f6a5835a..15d7de76 100644 --- a/debian/control +++ b/debian/control @@ -690,6 +690,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}, @@ -806,6 +822,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 From 3a401152149384b615d33686a5ef7f5599850085 Mon Sep 17 00:00:00 2001 From: Michael Zanetti Date: Thu, 10 Oct 2019 13:08:35 +0200 Subject: [PATCH 17/22] small fixes --- sonos/devicepluginsonos.cpp | 3 +-- sonos/devicepluginsonos.json | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/sonos/devicepluginsonos.cpp b/sonos/devicepluginsonos.cpp index 2fa4eb90..15388364 100644 --- a/sonos/devicepluginsonos.cpp +++ b/sonos/devicepluginsonos.cpp @@ -178,8 +178,7 @@ void DevicePluginSonos::confirmPairing(DevicePairingInfo *info, const QString &u return; } sonos->getAccessTokenFromAuthorizationCode(authorizationCode); - connect(sonos, &Sonos::authenticationStatusChanged, info, [this, info](bool authenticated){ - Sonos *sonos = static_cast(sender()); + connect(sonos, &Sonos::authenticationStatusChanged, info, [this, info, sonos](bool authenticated){ if(!authenticated) { qWarning(dcSonos()) << "Authentication process failed" << info->deviceName(); m_setupSonosConnections.remove(info->deviceId()); diff --git a/sonos/devicepluginsonos.json b/sonos/devicepluginsonos.json index 97b23875..c9087780 100644 --- a/sonos/devicepluginsonos.json +++ b/sonos/devicepluginsonos.json @@ -7,7 +7,7 @@ "id": "30a60752-d06f-4ec9-a4e1-9810a5d22fa3", "name": "sonos", - "displayName": "Sono", + "displayName": "Sonos", "deviceClasses": [ { "id": "22df416d-7732-44f1-b6b9-e41296211178", From 19c0a283664e8dc4dcd557c63edc32fecfcf4c0e Mon Sep 17 00:00:00 2001 From: nymea Date: Fri, 11 Oct 2019 13:49:48 +0200 Subject: [PATCH 18/22] added favorite browsing --- sonos/devicepluginsonos.cpp | 184 ++++++++++++++++++++++++++--------- sonos/devicepluginsonos.h | 10 +- sonos/devicepluginsonos.json | 54 ++++++---- sonos/sonos.cpp | 25 +++-- 4 files changed, 196 insertions(+), 77 deletions(-) diff --git a/sonos/devicepluginsonos.cpp b/sonos/devicepluginsonos.cpp index 15388364..16c0ce99 100644 --- a/sonos/devicepluginsonos.cpp +++ b/sonos/devicepluginsonos.cpp @@ -48,44 +48,6 @@ void DevicePluginSonos::setupDevice(DeviceSetupInfo *info) { Device *device = info->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; if (m_setupSonosConnections.keys().contains(device->id())) { @@ -126,6 +88,7 @@ void DevicePluginSonos::setupDevice(DeviceSetupInfo *info) connect(sonos, &Sonos::volumeReceived, this, &DevicePluginSonos::onVolumeReceived); connect(sonos, &Sonos::actionExecuted, this, &DevicePluginSonos::onActionExecuted); connect(sonos, &Sonos::authenticationStatusChanged, this, &DevicePluginSonos::onAuthenticationStatusChanged); + connect(sonos, &Sonos::favouritesReceived, this, &DevicePluginSonos::onFavouritesReceived); sonos->getAccessTokenFromRefreshToken(refreshToken); m_sonosConnections.insert(device, sonos); return info->finish(Device::DeviceErrorNoError); @@ -204,6 +167,44 @@ void DevicePluginSonos::confirmPairing(DevicePairingInfo *info, const QString &u 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(); @@ -331,6 +332,55 @@ void DevicePluginSonos::executeAction(DeviceActionInfo *info) info->finish(Device::DeviceErrorDeviceClassNotFound); } +void DevicePluginSonos::browseDevice(BrowseResult *result) +{ + Device *parentDevice = myDevices().findById(result->device()->parentId()); + Sonos *sonosConnection = m_sonosConnections.value(parentDevice); + if (!sonosConnection) + return; + + qDebug(dcSonos()) << "Browse Device" << result->itemId(); + QString householdId = result->device()->paramValue(sonosGroupDeviceHouseholdIdParamTypeId).toString(); + if (result->itemId().isEmpty()){ + BrowserItem item; + item.setId("favorites"); + item.setIcon(BrowserItem::BrowserIconFavorites); + item.setExecutable(false); + item.setBrowsable(true); + item.setDisplayName("Favorites"); + result->addItem(item); + result->finish(Device::DeviceErrorNoError); + } else if (result->itemId() == "favorites") { + sonosConnection->getFavorites(householdId); + m_pendingBrowseResult.insert(householdId, result); + } else { + //TODO add media browsing + result->finish(Device::DeviceErrorItemNotFound); + } +} + +void DevicePluginSonos::browserItem(BrowserItemResult *result) +{ + Q_UNUSED(result) +} + +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(); + QUuid requestId = sonosConnection->loadFavorite(groupId, info->browserAction().itemId()); + m_pendingBrowserExecution.insert(requestId, info); +} + +void DevicePluginSonos::executeBrowserItemAction(BrowserItemActionInfo *info) +{ + Q_UNUSED(info) +} + void DevicePluginSonos::onConnectionChanged(bool connected) { Sonos *sonos = static_cast(sender()); @@ -366,25 +416,45 @@ void DevicePluginSonos::onHouseholdIdsReceived(QList householdIds) Sonos *sonos = static_cast(sender()); foreach(QString householdId, householdIds) { sonos->getGroups(householdId); - sonos->getFavorites(householdId); sonos->getPlaylists(householdId); } } void DevicePluginSonos::onFavouritesReceived(const QString &householdId, QList favourites) { - Q_UNUSED(householdId); - foreach(Sonos::FavouriteObject favourite, favourites) { - qDebug(dcSonos()) << "Favourite: " << favourite.name << favourite.description; + if (m_pendingBrowseResult.contains(householdId)) { + BrowseResult *result = m_pendingBrowseResult.take(householdId); + if (!result) + return; + + foreach(Sonos::FavouriteObject favourite, favourites) { + BrowserItem item; + item.setId(favourite.id); + item.setExecutable(true); + item.setBrowsable(false); + if (!favourite.imageUrl.isEmpty()) { + item.setThumbnail(favourite.imageUrl); + } else { + item.setIcon(BrowserItem::BrowserIconFavorites); + } + item.setDisplayName(favourite.name); + item.setDescription(favourite.description); + result->addItem(item); + qDebug(dcSonos()) << "Favourite: " << favourite.name << favourite.description; + } + result->finish(Device::DeviceErrorNoError); + } else { + qDebug(dcSonos()) << "Received unhandled favourites list"; } } 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); + sonos->getPlaylist(householdId, playlist.id); //Get the playlist details } } @@ -399,7 +469,6 @@ void DevicePluginSonos::onPlaylistSummaryReceived(const QString &householdId, So void DevicePluginSonos::onGroupsReceived(const QString &householdId, QList groupObjects) { - Q_UNUSED(householdId); Sonos *sonos = static_cast(sender()); Device *parentDevice = m_sonosConnections.key(sonos); if (!parentDevice) @@ -409,12 +478,16 @@ void DevicePluginSonos::onGroupsReceived(const QString &householdId, QListsetName(groupObject.displayName); + 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); } @@ -478,8 +551,11 @@ void DevicePluginSonos::onMetadataStatusReceived(const QString &groupId, Sonos:: device->setStateValue(sonosGroupTitleStateTypeId, metaDataStatus.currentItem.track.name); device->setStateValue(sonosGroupArtistStateTypeId, metaDataStatus.currentItem.track.artist.name); device->setStateValue(sonosGroupCollectionStateTypeId, metaDataStatus.currentItem.track.album.name); - //device->setStateValue(sonosGroupArtworkStateTypeId, metaDataStatus.currentItem.track.imageUrl); - device->setStateValue(sonosGroupArtworkStateTypeId, metaDataStatus.container.imageUrl); + 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) @@ -506,4 +582,18 @@ void DevicePluginSonos::onActionExecuted(QUuid sonosActionId, bool success) 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 index f72d4796..97e6c6d6 100644 --- a/sonos/devicepluginsonos.h +++ b/sonos/devicepluginsonos.h @@ -49,11 +49,16 @@ public: 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; + void executeBrowserItemAction(BrowserItemActionInfo *info) override; + private: PluginTimer *m_pluginTimer5sec = nullptr; PluginTimer *m_pluginTimer60sec = nullptr; - QHash m_setupSonosConnections; + QHash m_setupSonosConnections; QHash m_sonosConnections; QList m_householdIds; @@ -61,6 +66,9 @@ private: QByteArray m_sonosConnectionRefreshToken; QHash > m_pendingActions; + QHash m_pendingBrowseResult; + QHash m_pendingBrowserExecution; + private slots: void onConnectionChanged(bool connected); diff --git a/sonos/devicepluginsonos.json b/sonos/devicepluginsonos.json index c9087780..2621d84e 100644 --- a/sonos/devicepluginsonos.json +++ b/sonos/devicepluginsonos.json @@ -22,8 +22,8 @@ { "id": "5aa4360c-61de-47d0-a72e-a19d57712e1c", "name": "connected", - "displayName": "connected", - "displayNameEvent": "connected changed", + "displayName": "Connected", + "displayNameEvent": "Connected changed", "defaultValue": true, "type": "bool" }, @@ -51,28 +51,35 @@ "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", + "displayName": "Connected", + "displayNameEvent": "Connected changed", "defaultValue": false, "type": "bool" }, { "id": "bc98cdb0-4d0e-48ca-afc7-922e49bb7813", "name": "mute", - "displayName": "mute", - "displayNameEvent": "mute changed", + "displayName": "Mute", + "displayNameEvent": "Mute changed", "displayNameAction": "Set mute", "type": "bool", "defaultValue": true, @@ -81,8 +88,8 @@ { "id": "9dfe5d78-4c3f-497c-bab1-bb9fdf7e93a9", "name": "volume", - "displayName": "volume", - "displayNameEvent": "volume changed", + "displayName": "Volume", + "displayNameEvent": "Volume changed", "displayNameAction": "Set volume", "unit": "Percentage", "type": "int", @@ -94,12 +101,12 @@ { "id": "2dd512b7-40c2-488e-8d4f-6519edaa6f74", "name": "playbackStatus", - "displayName": "playback status", + "displayName": "Playback status", "type": "QString", "possibleValues": ["Playing", "Paused", "Stopped"], "defaultValue": "Stopped", - "displayNameEvent": "playback status changed", - "displayNameAction": "set playback status", + "displayNameEvent": "Playback status changed", + "displayNameAction": "Set playback status", "writable": true }, { @@ -160,49 +167,56 @@ { "id": "2535a1eb-7643-4874-98f6-b027fdff6311", "name": "onPlayerPlay", - "displayName": "player play" + "displayName": "Group play" }, { "id": "99498b1c-e9c0-480a-9e91-662ee79ba976", "name": "onPlayerPause", - "displayName": "player pause" + "displayName": "Group pause" }, { "id": "a02ce255-3abb-435d-a92e-7f99c952ecb2", "name": "onPlayerStop", - "displayName": "player stop" + "displayName": "Group stop" } ], "actionTypes": [ { "id": "a180807d-1265-4831-9d86-a421767418dd", "name": "skipBack", - "displayName": "skip back" + "displayName": "Skip back" }, { "id": "7e70b47b-7e79-4521-be34-04a3c427e5b1", "name": "fastRewind", - "displayName": "rewind" + "displayName": "Rewind" }, { "id": "ae3cbe03-ee3e-410e-abbd-efabc2402198", "name": "stop", - "displayName": "stop" + "displayName": "Stop" }, { "id": "4d2ee668-a2e3-4795-8b96-0c800b703b46", "name": "play", - "displayName": "play" + "displayName": "Play" }, { "id": "3cf341cb-fe63-40bc-a450-9678d18e91e3", "name": "pause", - "displayName": "pause" + "displayName": "Pause" }, { "id": "85d7126a-b123-4a28-aeb4-d84bcfb4d14f", "name": "skipNext", - "displayName": "skipNext" + "displayName": "Skip Next" + } + ], + "browserItemActionTypes": [ + { + "id": "b056af0f-4d5b-4f75-b0dd-e73246d3a83f", + "name": "updateLibrary", + "displayName": "Update library" } ] } diff --git a/sonos/sonos.cpp b/sonos/sonos.cpp index 356a0f80..e533fe41 100644 --- a/sonos/sonos.cpp +++ b/sonos/sonos.cpp @@ -111,7 +111,7 @@ void Sonos::getHouseholds() QList households; foreach (const QVariant &variant, data.toVariant().toMap().value("households").toList()) { QVariantMap obj = variant.toMap(); - qDebug(dcSonos()) << "Household ID received:" << obj["id"].toString(); + //qDebug(dcSonos()) << "Household ID received:" << obj["id"].toString(); households.append(obj["id"].toString()); } emit householdIdsReceived(households); @@ -125,12 +125,14 @@ QUuid Sonos::loadFavorite(const QString &groupId, const QString &favouriteId) 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 + "/favourites")); + request.setUrl(QUrl(m_baseControlUrl + "/groups/" + groupId + "/favorites")); QUuid actionId = QUuid::createUuid(); QJsonObject object; - object.insert("favoriteId", QJsonValue::fromVariant(favouriteId)); + 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] { @@ -193,15 +195,16 @@ void Sonos::getFavorites(const QString &householdId) return; QVariantList array = data.toVariant().toMap().value("items").toList(); - QList favourites - ; + //qDebug(dcSonos()) << "Favourites received:" << data.toJson(); + + QList favourites; foreach (const QVariant &variant, array) { QVariantMap itemObject = variant.toMap(); - qDebug(dcSonos()) << "Item ID received:" << itemObject["id"].toString(); FavouriteObject favourite; favourite.id = itemObject["id"].toString(); favourite.name = itemObject["name"].toString(); favourite.description = itemObject["description"].toString(); + favourite.imageUrl = itemObject["imageUrl"].toString(); favourites.append(favourite); } emit favouritesReceived(householdId, favourites); @@ -248,10 +251,16 @@ void Sonos::getGroups(const QString &householdId) QList groupObjects; foreach (const QVariant &value, array) { QVariantMap obj = value.toMap(); - qDebug(dcSonos()) << "Group ID received:" << obj["id"].toString(); + //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); @@ -1289,7 +1298,6 @@ void Sonos::getPlaylist(const QString &householdId, const QString &playlistId) QVariantList array = variant["tracks"].toList(); foreach (const QVariant &value, array) { QVariantMap itemObject = value.toMap(); - qDebug(dcSonos()) << "Item ID received:" << itemObject["id"].toString(); PlaylistTrackObject track; track.name = itemObject["name"].toString(); track.album = itemObject["album"].toString(); @@ -1341,7 +1349,6 @@ void Sonos::getPlaylists(const QString &householdId) QList playlists; foreach (const QVariant &value, array) { QVariantMap itemObject = value.toMap(); - qDebug(dcSonos()) << "Item ID received:" << itemObject["id"].toString(); PlaylistObject playlist; playlist.id = itemObject["id"].toString(); playlist.name = itemObject["name"].toString(); From 83003d02704f0766489498f171782ce41ee6569c Mon Sep 17 00:00:00 2001 From: Boernsman Date: Fri, 18 Oct 2019 18:46:26 +0200 Subject: [PATCH 19/22] fixed deleting of pending browse requests --- sonos/devicepluginsonos.cpp | 11 +++++------ sonos/devicepluginsonos.h | 1 - sonos/devicepluginsonos.json | 5 ----- 3 files changed, 5 insertions(+), 12 deletions(-) diff --git a/sonos/devicepluginsonos.cpp b/sonos/devicepluginsonos.cpp index 16c0ce99..fb2bbd21 100644 --- a/sonos/devicepluginsonos.cpp +++ b/sonos/devicepluginsonos.cpp @@ -24,6 +24,8 @@ #include "devices/device.h" #include "network/networkaccessmanager.h" #include "plugininfo.h" +#include "types/mediabrowseritem.h" + #include #include @@ -353,6 +355,7 @@ void DevicePluginSonos::browseDevice(BrowseResult *result) } else if (result->itemId() == "favorites") { sonosConnection->getFavorites(householdId); m_pendingBrowseResult.insert(householdId, result); + connect(result, &BrowseResult::aborted,[householdId, this](){m_pendingBrowseResult.remove(householdId);}); } else { //TODO add media browsing result->finish(Device::DeviceErrorItemNotFound); @@ -374,11 +377,7 @@ void DevicePluginSonos::executeBrowserItem(BrowserActionInfo *info) QString groupId = info->device()->paramValue(sonosGroupDeviceGroupIdParamTypeId).toString(); QUuid requestId = sonosConnection->loadFavorite(groupId, info->browserAction().itemId()); m_pendingBrowserExecution.insert(requestId, info); -} - -void DevicePluginSonos::executeBrowserItemAction(BrowserItemActionInfo *info) -{ - Q_UNUSED(info) + connect(info, &BrowserActionInfo::aborted,[requestId, this](){m_pendingBrowserExecution.remove(requestId);}); } void DevicePluginSonos::onConnectionChanged(bool connected) @@ -428,7 +427,7 @@ void DevicePluginSonos::onFavouritesReceived(const QString &householdId, QList Date: Fri, 18 Oct 2019 20:45:59 +0200 Subject: [PATCH 20/22] removed browser actions, added browse item --- sonos/devicepluginsonos.cpp | 61 ++++++++++++++++++++++++++++++------ sonos/devicepluginsonos.h | 1 + sonos/devicepluginsonos.json | 5 +-- 3 files changed, 55 insertions(+), 12 deletions(-) diff --git a/sonos/devicepluginsonos.cpp b/sonos/devicepluginsonos.cpp index fb2bbd21..9c617e17 100644 --- a/sonos/devicepluginsonos.cpp +++ b/sonos/devicepluginsonos.cpp @@ -81,6 +81,11 @@ void DevicePluginSonos::setupDevice(DeviceSetupInfo *info) 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); @@ -345,14 +350,14 @@ void DevicePluginSonos::browseDevice(BrowseResult *result) QString householdId = result->device()->paramValue(sonosGroupDeviceHouseholdIdParamTypeId).toString(); if (result->itemId().isEmpty()){ BrowserItem item; - item.setId("favorites"); + item.setId("/favorites"); item.setIcon(BrowserItem::BrowserIconFavorites); item.setExecutable(false); item.setBrowsable(true); item.setDisplayName("Favorites"); result->addItem(item); result->finish(Device::DeviceErrorNoError); - } else if (result->itemId() == "favorites") { + } else if (result->itemId() == "/favorites") { sonosConnection->getFavorites(householdId); m_pendingBrowseResult.insert(householdId, result); connect(result, &BrowseResult::aborted,[householdId, this](){m_pendingBrowseResult.remove(householdId);}); @@ -364,7 +369,18 @@ void DevicePluginSonos::browseDevice(BrowseResult *result) void DevicePluginSonos::browserItem(BrowserItemResult *result) { - Q_UNUSED(result) + Device *parentDevice = myDevices().findById(result->device()->parentId()); + Sonos *sonosConnection = m_sonosConnections.value(parentDevice); + if (!sonosConnection) + return; + + qCDebug(dcSonos()) << "Browser Item" << result->itemId(); + QString householdId = result->device()->paramValue(sonosGroupDeviceHouseholdIdParamTypeId).toString(); + if (result->itemId().startsWith("/favorites")) { + sonosConnection->getFavorites(householdId); + m_pendingBrowserItemResult.insert(householdId, result); + connect(result, &BrowserItemResult::aborted,[householdId, this](){m_pendingBrowserItemResult.remove(householdId);}); + } } void DevicePluginSonos::executeBrowserItem(BrowserActionInfo *info) @@ -375,9 +391,12 @@ void DevicePluginSonos::executeBrowserItem(BrowserActionInfo *info) return; QString groupId = info->device()->paramValue(sonosGroupDeviceGroupIdParamTypeId).toString(); - QUuid requestId = sonosConnection->loadFavorite(groupId, info->browserAction().itemId()); - m_pendingBrowserExecution.insert(requestId, info); - connect(info, &BrowserActionInfo::aborted,[requestId, this](){m_pendingBrowserExecution.remove(requestId);}); + if (info->browserAction().itemId().startsWith("/favorites")) { + QString favoriteId = info->browserAction().itemId().remove("/favorite/"); + QUuid requestId = sonosConnection->loadFavorite(groupId, favoriteId); + m_pendingBrowserExecution.insert(requestId, info); + connect(info, &BrowserActionInfo::aborted,[requestId, this](){m_pendingBrowserExecution.remove(requestId);}); + } } void DevicePluginSonos::onConnectionChanged(bool connected) @@ -428,11 +447,11 @@ void DevicePluginSonos::onFavouritesReceived(const QString &householdId, QListitemId() + "/" + favourite.id); item.setExecutable(true); item.setBrowsable(false); if (!favourite.imageUrl.isEmpty()) { - item.setThumbnail(favourite.imageUrl); + item.setThumbnail(favourite.imageUrl); } else { item.setIcon(BrowserItem::BrowserIconFavorites); } @@ -442,8 +461,30 @@ void DevicePluginSonos::onFavouritesReceived(const QString &householdId, QListfinish(Device::DeviceErrorNoError); - } else { - qDebug(dcSonos()) << "Received unhandled favourites list"; + } + + if (m_pendingBrowserItemResult.contains(householdId)) { + BrowserItemResult *result = m_pendingBrowserItemResult.take(householdId); + if (!result) + return; + QString favoriteId = result->itemId().remove("/favorites/"); + + foreach(Sonos::FavouriteObject favourite, favourites) { + if (favourite.id == favoriteId) { + MediaBrowserItem item; + item.setId(result->itemId()); + item.setExecutable(true); + item.setBrowsable(false); + if (!favourite.imageUrl.isEmpty()) { + item.setThumbnail(favourite.imageUrl); + } else { + item.setIcon(BrowserItem::BrowserIconFavorites); + } + item.setDisplayName(favourite.name); + item.setDescription(favourite.description); + result->finish(item); + } + } } } diff --git a/sonos/devicepluginsonos.h b/sonos/devicepluginsonos.h index 9e0c76fa..b8923478 100644 --- a/sonos/devicepluginsonos.h +++ b/sonos/devicepluginsonos.h @@ -66,6 +66,7 @@ private: QHash > m_pendingActions; QHash m_pendingBrowseResult; + QHash m_pendingBrowserItemResult; QHash m_pendingBrowserExecution; diff --git a/sonos/devicepluginsonos.json b/sonos/devicepluginsonos.json index ed5534e4..c357fd9c 100644 --- a/sonos/devicepluginsonos.json +++ b/sonos/devicepluginsonos.json @@ -24,8 +24,9 @@ "name": "connected", "displayName": "Connected", "displayNameEvent": "Connected changed", - "defaultValue": true, - "type": "bool" + "defaultValue": false, + "type": "bool", + "cached": false }, { "id": "48b5c1bf-7df0-45d0-9ba3-290fc3acddc3", From 0a388cd0124654f25f3dc6384fd186676aaa4a75 Mon Sep 17 00:00:00 2001 From: Boernsman Date: Mon, 21 Oct 2019 08:28:27 +0200 Subject: [PATCH 21/22] fixed browsing item to request ID association --- sonos/devicepluginsonos.cpp | 84 +++++++++++++++++++++---------------- sonos/devicepluginsonos.h | 7 ++-- sonos/sonos.cpp | 25 ++++++----- sonos/sonos.h | 10 ++--- 4 files changed, 72 insertions(+), 54 deletions(-) diff --git a/sonos/devicepluginsonos.cpp b/sonos/devicepluginsonos.cpp index 9c617e17..04b08355 100644 --- a/sonos/devicepluginsonos.cpp +++ b/sonos/devicepluginsonos.cpp @@ -1,4 +1,4 @@ -/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * Copyright (C) 2019 Bernhard Trinnes getAccessTokenFromRefreshToken(refreshToken); m_sonosConnections.insert(device, sonos); return info->finish(Device::DeviceErrorNoError); @@ -343,24 +343,26 @@ void DevicePluginSonos::browseDevice(BrowseResult *result) { Device *parentDevice = myDevices().findById(result->device()->parentId()); Sonos *sonosConnection = m_sonosConnections.value(parentDevice); - if (!sonosConnection) + 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("/favorites"); + 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() == "/favorites") { - sonosConnection->getFavorites(householdId); - m_pendingBrowseResult.insert(householdId, result); - connect(result, &BrowseResult::aborted,[householdId, this](){m_pendingBrowseResult.remove(householdId);}); + } 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); @@ -371,15 +373,20 @@ void DevicePluginSonos::browserItem(BrowserItemResult *result) { Device *parentDevice = myDevices().findById(result->device()->parentId()); Sonos *sonosConnection = m_sonosConnections.value(parentDevice); - if (!sonosConnection) + if (!sonosConnection) { + result->finish(Device::DeviceErrorHardwareNotAvailable); return; + } qCDebug(dcSonos()) << "Browser Item" << result->itemId(); QString householdId = result->device()->paramValue(sonosGroupDeviceHouseholdIdParamTypeId).toString(); - if (result->itemId().startsWith("/favorites")) { - sonosConnection->getFavorites(householdId); - m_pendingBrowserItemResult.insert(householdId, result); - connect(result, &BrowserItemResult::aborted,[householdId, this](){m_pendingBrowserItemResult.remove(householdId);}); + 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); } } @@ -391,11 +398,15 @@ void DevicePluginSonos::executeBrowserItem(BrowserActionInfo *info) return; QString groupId = info->device()->paramValue(sonosGroupDeviceGroupIdParamTypeId).toString(); - if (info->browserAction().itemId().startsWith("/favorites")) { - QString favoriteId = info->browserAction().itemId().remove("/favorite/"); + 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); } } @@ -438,51 +449,54 @@ void DevicePluginSonos::onHouseholdIdsReceived(QList householdIds) } } -void DevicePluginSonos::onFavouritesReceived(const QString &householdId, QList favourites) +void DevicePluginSonos::onFavoritesReceived(QUuid requestId, const QString &householdId, QList favorites) { - if (m_pendingBrowseResult.contains(householdId)) { - BrowseResult *result = m_pendingBrowseResult.take(householdId); + Q_UNUSED(householdId) + + if (m_pendingBrowseResult.contains(requestId)) { + BrowseResult *result = m_pendingBrowseResult.take(requestId); if (!result) return; - foreach(Sonos::FavouriteObject favourite, favourites) { + foreach(Sonos::FavoriteObject favorite, favorites) { MediaBrowserItem item; - item.setId(result->itemId() + "/" + favourite.id); + item.setId(result->itemId() + "/" + favorite.id); item.setExecutable(true); item.setBrowsable(false); - if (!favourite.imageUrl.isEmpty()) { - item.setThumbnail(favourite.imageUrl); + if (!favorite.imageUrl.isEmpty()) { + item.setThumbnail(favorite.imageUrl); } else { item.setIcon(BrowserItem::BrowserIconFavorites); } - item.setDisplayName(favourite.name); - item.setDescription(favourite.description); + item.setDisplayName(favorite.name); + item.setDescription(favorite.description); result->addItem(item); - qDebug(dcSonos()) << "Favourite: " << favourite.name << favourite.description; + qDebug(dcSonos()) << "Favorite: " << favorite.name << favorite.description; } result->finish(Device::DeviceErrorNoError); - } - if (m_pendingBrowserItemResult.contains(householdId)) { - BrowserItemResult *result = m_pendingBrowserItemResult.take(householdId); + } else if (m_pendingBrowserItemResult.contains(requestId)) { + BrowserItemResult *result = m_pendingBrowserItemResult.take(requestId); if (!result) return; - QString favoriteId = result->itemId().remove("/favorites/"); + QString favoriteId = result->itemId().remove(m_browseFavoritesPrefix); + favoriteId.remove('/'); - foreach(Sonos::FavouriteObject favourite, favourites) { - if (favourite.id == favoriteId) { + foreach(Sonos::FavoriteObject favorite, favorites) { + if (favorite.id == favoriteId) { MediaBrowserItem item; item.setId(result->itemId()); item.setExecutable(true); item.setBrowsable(false); - if (!favourite.imageUrl.isEmpty()) { - item.setThumbnail(favourite.imageUrl); + if (!favorite.imageUrl.isEmpty()) { + item.setThumbnail(favorite.imageUrl); } else { item.setIcon(BrowserItem::BrowserIconFavorites); } - item.setDisplayName(favourite.name); - item.setDescription(favourite.description); + item.setDisplayName(favorite.name); + item.setDescription(favorite.description); result->finish(item); + return; } } } diff --git a/sonos/devicepluginsonos.h b/sonos/devicepluginsonos.h index b8923478..a23cf3f7 100644 --- a/sonos/devicepluginsonos.h +++ b/sonos/devicepluginsonos.h @@ -65,17 +65,18 @@ private: QByteArray m_sonosConnectionRefreshToken; QHash > m_pendingActions; - QHash m_pendingBrowseResult; - QHash m_pendingBrowserItemResult; + 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 onFavouritesReceived(const QString &householdId, QList favourites); + void onFavoritesReceived(QUuid requestId, const QString &householdId, QList favorites); void onPlaylistsReceived(const QString &householdId, QList playlists); void onPlaylistSummaryReceived(const QString &householdId, Sonos::PlaylistSummaryObject playlistSummary); diff --git a/sonos/sonos.cpp b/sonos/sonos.cpp index e533fe41..30f9a857 100644 --- a/sonos/sonos.cpp +++ b/sonos/sonos.cpp @@ -158,15 +158,17 @@ QUuid Sonos::loadFavorite(const QString &groupId, const QString &favouriteId) return actionId; } -void Sonos::getFavorites(const QString &householdId) +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, householdId, this] { + connect(reply, &QNetworkReply::finished, this, [reply, requestId, householdId, this] { reply->deleteLater(); int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); @@ -195,20 +197,21 @@ void Sonos::getFavorites(const QString &householdId) return; QVariantList array = data.toVariant().toMap().value("items").toList(); - //qDebug(dcSonos()) << "Favourites received:" << data.toJson(); + //qDebug(dcSonos()) << "Favorites received:" << data.toJson(); - QList favourites; + QList favorites; foreach (const QVariant &variant, array) { QVariantMap itemObject = variant.toMap(); - FavouriteObject favourite; - favourite.id = itemObject["id"].toString(); - favourite.name = itemObject["name"].toString(); - favourite.description = itemObject["description"].toString(); - favourite.imageUrl = itemObject["imageUrl"].toString(); - favourites.append(favourite); + 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 favouritesReceived(householdId, favourites); + emit favoritesReceived(requestId, householdId, favorites); }); + return requestId; } diff --git a/sonos/sonos.h b/sonos/sonos.h index 8d6d7cc7..cb21b678 100644 --- a/sonos/sonos.h +++ b/sonos/sonos.h @@ -81,7 +81,7 @@ public: /* * 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 FavouriteObject { + struct FavoriteObject { QString id; QString name; QString description; @@ -216,10 +216,10 @@ public: void getAccessTokenFromAuthorizationCode(const QByteArray &authorizationCode); void getHouseholds(); - void getFavorites(const QString &householdId); + QUuid getFavorites(const QString &householdId); void getGroups(const QString &householdId); - QUuid loadFavorite(const QString &groupId, const QString &favouriteId); + QUuid loadFavorite(const QString &groupId, const QString &faveriteId); //Group volume void getGroupVolume(const QString &groupId); //Get the volume and mute state of a group. @@ -283,7 +283,7 @@ signals: void authenticationStatusChanged(bool authenticated); void householdIdsReceived(QList householdIds); - void favouritesReceived(const QString &householdId, QList favourites); + 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); @@ -294,6 +294,6 @@ signals: void playerVolumeReceived(const QString &playerId, VolumeObject playerVolume); void playerSettingsRecieved(const QString &playerId, PlayerSettingsObject playerSettings); - void actionExecuted(QUuid actionId,bool success); + void actionExecuted(QUuid actionId, bool success); }; #endif // SONOS_H From 058fef115bcf99d2a9ca433aa3595feb3672c546 Mon Sep 17 00:00:00 2001 From: Boernsman Date: Mon, 21 Oct 2019 19:55:28 +0200 Subject: [PATCH 22/22] deleted .gitmodules file --- .gitmodules | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 .gitmodules diff --git a/.gitmodules b/.gitmodules deleted file mode 100644 index e69de29b..00000000