From f7284bd57e5844f1bad3473c1c5ba888289f52b7 Mon Sep 17 00:00:00 2001 From: nymea Date: Wed, 10 Jul 2019 23:12:50 +0200 Subject: [PATCH 1/9] controls work, status missing --- bose/soundtouch.cpp | 318 +++++++++++++++++++++++++++++++++++++++++ bose/soundtouch.h | 67 +++++++++ bose/soundtouchtypes.h | 220 ++++++++++++++++++++++++++++ nymea-plugins.pro | 1 + 4 files changed, 606 insertions(+) create mode 100644 bose/soundtouch.cpp create mode 100644 bose/soundtouch.h create mode 100644 bose/soundtouchtypes.h diff --git a/bose/soundtouch.cpp b/bose/soundtouch.cpp new file mode 100644 index 00000000..9af989bf --- /dev/null +++ b/bose/soundtouch.cpp @@ -0,0 +1,318 @@ +#include "soundtouch.h" +#include "hardwaremanager.h" +#include "devices/device.h" +#include "network/networkaccessmanager.h" + +SoundTouch::SoundTouch(NetworkAccessManager *networkAccessManager, QString ipAddress, QObject *parent) : + QObject(parent), + m_networkAccessManager(networkAccessManager), + m_ipAddress(ipAddress) +{ + m_websocket = new QWebSocket(); + connect(m_websocket, &QWebSocket::connected, this, &SoundTouch::onWebsocketConnected); + connect(m_websocket, &QWebSocket::disconnected, this, &SoundTouch::onWebsocketDisconnected); + connect(m_websocket, &QWebSocket::textMessageReceived, this, &SoundTouch::onWebsocketMessageReceived); + QUrl url; + url.setHost(m_ipAddress); + url.setScheme("ws"); + url.setPort(8080); + qDebug(dcBose) << "Connecting websocket to" << url; + //TODO missing websocket subprotocol "gabbo" + m_websocket->open(url); +} + +void SoundTouch::getInfo() +{ + QUrl url; + url.setHost(m_ipAddress); + url.setScheme("http"); + url.setPort(m_port); + url.setPath("/info"); + qDebug(dcBose) << "Sending request" << url; + QNetworkReply *reply = m_networkAccessManager->get(QNetworkRequest(url)); + connect(reply, &QNetworkReply::finished, this, &SoundTouch::onRestRequestFinished); +} + +void SoundTouch::getVolume() +{ + QUrl url; + url.setHost(m_ipAddress); + url.setScheme("http"); + url.setPort(m_port); + url.setPath("/volume"); + qDebug(dcBose) << "Sending request" << url; + QNetworkReply *reply = m_networkAccessManager->get(QNetworkRequest(url)); + connect(reply, &QNetworkReply::finished, this, &SoundTouch::onRestRequestFinished); +} + +void SoundTouch::getNowPlaying() +{ + QUrl url; + url.setHost(m_ipAddress); + url.setScheme("http"); + url.setPort(m_port); + url.setPath("/now_playing"); + qDebug(dcBose) << "Sending request" << url; + QNetworkReply *reply = m_networkAccessManager->get(QNetworkRequest(url)); + connect(reply, &QNetworkReply::finished, this, &SoundTouch::onRestRequestFinished); +} + +void SoundTouch::getBass() +{ + QUrl url; + url.setHost(m_ipAddress); + url.setScheme("http"); + url.setPort(m_port); + url.setPath("/bass"); + qDebug(dcBose) << "Sending request" << url; + QNetworkReply *reply = m_networkAccessManager->get(QNetworkRequest(url)); + connect(reply, &QNetworkReply::finished, this, &SoundTouch::onRestRequestFinished); +} + +void SoundTouch::getGroup() +{ + QUrl url; + url.setHost(m_ipAddress); + url.setScheme("http"); + url.setPort(m_port); + url.setPath("/group"); + qDebug(dcBose) << "Sending request" << url; + QNetworkReply *reply = m_networkAccessManager->get(QNetworkRequest(url)); + connect(reply, &QNetworkReply::finished, this, &SoundTouch::onRestRequestFinished); +} + +void SoundTouch::getSources() +{ + +} + +void SoundTouch::getZone() +{ + +} + +void SoundTouch::setKey(KEY_VALUE keyValue) +{ + QUrl url; + url.setHost(m_ipAddress); + url.setScheme("http"); + url.setPort(m_port); + url.setPath("/key"); + QByteArray content = (""); + content.append(""); + switch (keyValue){ + case KEY_VALUE_PLAY: + content.append("PLAY"); + break; + case KEY_VALUE_STOP: + content.append("STOP"); + break; + case KEY_VALUE_PLAY_PAUSE: + content.append("PLAY_PAUSE"); + break; + case KEY_VALUE_POWER: + content.append("POWER"); + break; + case KEY_VALUE_NEXT_TRACK: + content.append("NEXT_TRACK"); + break; + case KEY_VALUE_PREV_TRACK: + content.append("PREV_TRACK"); + break; + case KEY_VALUE_BOOKMARK: + content.append("BOOKMARK"); + break; + case KEY_VALUE_AUX_INPUT: + content.append("AUX_INPUT"); + break; + case KEY_VALUE_REPEAT_ALL: + content.append("REPEAT_ALL"); + break; + case KEY_VALUE_REPEAT_ONE: + content.append("REPEAT_ONE"); + break; + case KEY_VALUE_REPEAT_OFF: + content.append("REPEAT_OFF"); + break; + case KEY_VALUE_ADD_FAVORITE: + content.append("ADD_FAVORITE"); + break; + case KEY_VALUE_MUTE: + content.append("MUTE"); + break; + case KEY_VALUE_SHUFFLE_ON: + content.append("SHUFFLE_ON"); + break; + case KEY_VALUE_SHUFFLE_OFF: + content.append("SHUFFLE_OFF"); + break; + default: + qWarning(dcBose) << "key not yet implemented"; + return; + } + content.append(""); + qDebug(dcBose) << "Sending request" << url << content; + QNetworkReply *reply = m_networkAccessManager->post(QNetworkRequest(url), content); + connect(reply, &QNetworkReply::finished, this, &SoundTouch::onRestRequestFinished); + + if (keyValue == KEY_VALUE_POWER) { + QUrl url; + url.setHost(m_ipAddress); + url.setScheme("http"); + url.setPort(m_port); + url.setPath("/key"); + QByteArray content = (""); + content.append(""); + content.append("POWER"); + content.append(""); + qDebug(dcBose) << "Sending request" << url << content; + QNetworkReply *reply = m_networkAccessManager->post(QNetworkRequest(url), content); + connect(reply, &QNetworkReply::finished, this, &SoundTouch::onRestRequestFinished); + } +} + +void SoundTouch::setVolume(int volume) +{ + QUrl url; + url.setHost(m_ipAddress); + url.setScheme("http"); + url.setPort(m_port); + url.setPath("/volume"); + QByteArray content = (""); + content.append(""); + content.append(QByteArray::number(volume)); + content.append(""); + qDebug(dcBose) << "Sending request" << url << content; + QNetworkReply *reply = m_networkAccessManager->post(QNetworkRequest(url), content); + connect(reply, &QNetworkReply::finished, this, &SoundTouch::onRestRequestFinished); +} + +void SoundTouch::setName(QString name) +{ + QUrl url; + url.setHost(m_ipAddress); + url.setScheme("http"); + url.setPort(m_port); + url.setPath("/name"); + QByteArray content = (""); + content.append(""); + content.append(name); + content.append(""); + qDebug(dcBose) << "Sending request" << url << content; + QNetworkReply *reply = m_networkAccessManager->post(QNetworkRequest(url), content); + connect(reply, &QNetworkReply::finished, this, &SoundTouch::onRestRequestFinished); +} + +void SoundTouch::onWebsocketConnected() +{ + qDebug(dcBose) << "Bose websocket connected"; + emit connectionChanged(true); +} + +void SoundTouch::onWebsocketDisconnected() +{ + qDebug(dcBose) << "Bose websocket disconnected"; + emit connectionChanged(false); + QTimer::singleShot(5000, [this](){ + QUrl url; + url.setHost(m_ipAddress); + url.setScheme("ws"); + url.setPort(8080); + m_websocket->open(url); + }); +} + + +void SoundTouch::onRestRequestFinished() { + + QNetworkReply *reply = static_cast(sender()); + reply->deleteLater(); + + QByteArray data = reply->readAll(); + qDebug(dcBose) << "REST message received:" << data; + int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); + + // Check HTTP status code + if (status != 200 || reply->error() != QNetworkReply::NoError) { + qCWarning(dcBose()) << "Request error:" << status << reply->errorString(); + } + + QXmlStreamReader xml; + xml.addData(data); + + if (xml.readNextStartElement()) { + if (xml.name() == "info") { + InfoObject info; + qDebug(dcBose) << "Info Request"; + if(xml.attributes().hasAttribute("deviceID")) { + qDebug(dcBose) << "Device ID" << xml.attributes().value("deviceID").toString(); + info.deviceID = xml.attributes().value("deviceID").toString(); + } + while(xml.readNextStartElement()){ + if(xml.name() == "name"){ + qDebug(dcBose) << "name" << xml.readElementText(); + info.name = xml.readElementText(); + } else if(xml.name() == "type"){ + qDebug(dcBose) << "type" << xml.readElementText(); + info.type = xml.readElementText(); + } else if(xml.name() == "components"){ + qDebug(dcBose) << "components element"; + while(xml.readNextStartElement()){ + if(xml.name() == "component"){ + while(xml.readNextStartElement()){ + if(xml.name() == "softwareVersion"){ + qDebug(dcBose) << "Software version" << xml.readElementText(); + } else if(xml.name() == "serialNumber") { + qDebug(dcBose) << "Serialnumber" << xml.readElementText(); + } else { + xml.skipCurrentElement(); + } + } + } else { + xml.skipCurrentElement(); + } + } + } else if(xml.name() == "networkInfo"){ + qDebug(dcBose) << "network Info"; + while (xml.readNextStartElement()) { + if (xml.name() == "macAddress") { + qDebug(dcBose) << "macAddress" << xml.readElementText(); + } else if(xml.name() == "ipAddress") { + qDebug(dcBose) << "ipAddress" << xml.readElementText(); + } else { + xml.skipCurrentElement(); + } + } + } else { + xml.skipCurrentElement(); + } + } + emit infoReceived(info); + } else if (xml.name() == "now_playing") { + NowPlayingObject nowPlaying; + + emit + } + } + + /*while (!xml.atEnd()) { + if (xml.readNext()) { + if (xml.tokenType() == TokenType::) + qDebug(dcBose) << "element" << xml.tokenString() << xml.readElementText(); + } + + if (xml.hasError()) { + qCWarning(dcBose()) << "Error when parsing XML response"; + } + }*/ + + + //} + +} + + +void SoundTouch::onWebsocketMessageReceived(QString message) +{ + qDebug(dcBose) << "Websocket message received:" << message; +} diff --git a/bose/soundtouch.h b/bose/soundtouch.h new file mode 100644 index 00000000..e9416cec --- /dev/null +++ b/bose/soundtouch.h @@ -0,0 +1,67 @@ +#ifndef SOUNDTOUCH_H +#define SOUNDTOUCH_H + +#include +#include +#include + +#include "extern-plugininfo.h" +#include "soundtouchtypes.h" + +#include "hardwaremanager.h" +#include "network/networkaccessmanager.h" + + +class SoundTouch : public QObject +{ + Q_OBJECT +public: + explicit SoundTouch(NetworkAccessManager *networkAccessManager, QString ipAddress, QObject *parent = nullptr); + + void getInfo(); //Get basic information about a product. + void getVolume(); //Get the current volume and mute status of a product. + void getNowPlaying(); //Get information about what's playing on a product. + void getBass(); //Get the bass level of a product, if supported. + void getGroup(); //Get the current left/right stereo pair configuration of a product. + void getSources(); //Get available sources for a product. + void getZone(); //Get the current multiroom zone state of a product. + //void getPresets(); //Get information related to the user's SoundTouch presets. + //void getBassCapabilities(); //Get whether a product supports reducing bass. + //void speaker(); //initiate playback of a specified network-accessible audio file on a Bose SoundTouch product. + void setKey(KEY_VALUE keyValue); //Start and control playback on a product. + void setVolume(int volume); //Set the volume of a product. + void setSource(SOURCE_STATUS source); //Select a product's source. + /*void setZone(QString zone); //Create a zone of synced products. + void addZoneSlave(); //Add one or more slave product(s) to a multiroom zone. + void removeZoneSlave(); //Remove one or more slave product(s) from a multiroom zone. + void setBass(int volume); //Set the bass level of a product, if supported.*/ + void setName(QString name); //Set the products user-facing name. + +private: + NetworkAccessManager *m_networkAccessManager = nullptr; + QString m_ipAddress; + int m_port = 8090; + QWebSocket *m_websocket = nullptr; + //QHash m_requests; + +signals: + void connectionChanged(bool connected); + void infoReceived(InfoObject info); + void nowPlayingReceived(NowPlayingObject nowPlaying); + void volumeReceived(VolumeObject volume); + void sourcesReceived(SourcesObject sources); + void zoneReceived(ZoneObject); + void bassCapabilitiesReceived(BassCapabilitiesObject bassCapabilities); + void bassReceived(BassObject bass); + void presetsReceived(PresetObject presets); + void groupReceived(GroupObject group); + + +private slots: + void onWebsocketConnected(); + void onWebsocketDisconnected(); + void onWebsocketMessageReceived(QString message); + void onRestRequestFinished(); +}; + +#endif // SOUNDTOUCH_H diff --git a/bose/soundtouchtypes.h b/bose/soundtouchtypes.h new file mode 100644 index 00000000..d4724d40 --- /dev/null +++ b/bose/soundtouchtypes.h @@ -0,0 +1,220 @@ +#ifndef SOUNDTOUCHTYPES_H +#define SOUNDTOUCHTYPES_H + +#include "extern-plugininfo.h" + +enum SHUFFLE_STATUS { + SHUFFLE_STATUS_SHUFFLE_OFF, + SHUFFLE_STATUS_SHUFFLE_ON +}; + +enum REPEAT_STATUS { //The state of repeat. + REPEAT_STATUS_REPEAT_OFF, + REPEAT_STATUS_REPEAT_ALL, + REPEAT_STATUS_REPEAT_ONE +}; + +enum STREAM_STATUS { //The type of music container that is streaming. + STREAM_STATUS_TRACK_ONDEMAND, + STREAM_STATUS_RADIO_STREAMING, + STREAM_STATUS_RADIO_TRACKS, + STREAM_STATUS_NO_TRANSPORT_CONTROLS +}; + +enum SOURCE_STATUS { //The availability of an audio source + SOURCE_STATUS_UNAVAILABLE, + SOURCE_STATUS_READY +}; + +enum PLAY_STATUS { //The state of the audio stream + PLAY_STATUS_PLAY_STATE, + PLAY_STATUS_PAUSE_STATE, + PLAY_STATUS_STOP_STATE, + PLAY_STATUS_BUFFERING_STATE +}; + +enum ART_STATUS { //The state of an image + ART_STATUS_INVALID, + ART_STATUS_SHOW_DEFAULT_IMAGE, + ART_STATUS_DOWNLOADING, + ART_STATUS_IMAGE_PRESENT +}; + +enum KEY_VALUE { //An enumeration of virtual device buttons that may be pressed + KEY_VALUE_PLAY, + KEY_VALUE_PAUSE, + KEY_VALUE_PLAY_PAUSE, + KEY_VALUE_STOP, + KEY_VALUE_PREV_TRACK, + KEY_VALUE_NEXT_TRACK, + KEY_VALUE_POWER, + KEY_VALUE_MUTE, + KEY_VALUE_AUX_INPUT, + KEY_VALUE_SHUFFLE_ON, + KEY_VALUE_SHUFFLE_OFF, + KEY_VALUE_REPEAT_ONE, + KEY_VALUE_REPEAT_ALL, + KEY_VALUE_REPEAT_OFF, + KEY_VALUE_ADD_FAVORITE, + KEY_VALUE_REMOVE_FAVORITE, + KEY_VALUE_THUMBS_UP, + KEY_VALUE_THUMBS_DOWN, + KEY_VALUE_BOOKMARK, + KEY_VALUE_PRESET_1, + KEY_VALUE_PRESET_2, + KEY_VALUE_PRESET_3, + KEY_VALUE_PRESET_4, + KEY_VALUE_PRESET_5, + KEY_VALUE_PRESET_6 +}; + +enum KEY_STATE { //The state of the virtual key press + KEY_STATE_PRESS, + KEY_STATE_RELEASE +}; + +enum GROUP_LOC { + GROUP_LOC_LEFT, + GROUP_LOC_RIGHT +}; + +struct ComponentObject { + QString softwareVersion; //Element. The firmware version of the component. + QString serialNumber; //Element. The serial number of the component. +}; + +struct NetworkInfoObject { + QString macAddress; //Element. The MAC Address of the component. + QString ipAddress; //Element. The IP Address of the product. +}; + +struct InfoObject { + QString deviceID; //Attribute. Unique identifier of the product. + QString name; //Element. The user-set product name. You can change this with /name. + QString type; //Element. The Bose-defined name for the product. Ex: SoundTouch 10. + QList components; //Element. A container for all component objects. See component object. + NetworkInfoObject networkInfo; //Element. This object describes the product's current connection information. See networkInfo object. +}; + +struct ArtObject { + ART_STATUS artStatus; + QString url; +}; + +struct TimeObject { + int total; //Attribute. Tells you the current time the track has played, in seconds. + int length; //Content. The length of the track, in seconds. +}; +struct ContentItemObject { + QString source; //Attribute. The type or name of the service playing. To determine if the product is in standby mode, check if source == STANDBY. + QString location; //Attribute. READ-ONLY. This attribute is used by Bose to point to the content the user is accessing. You can save it to access content later, but do not attempt to generate your own locations. + QString sourceAccount; //Attribute. The user-account associated with this source. + bool isPresetable; //Attribute. TRUE if the source can be set as one of the six (6) SoundTouch presets. + QString itemName; //Element. The album, station, playlist, song, phone, etc. name depending on the source. + QString containerArt; //URL +}; + +struct ConnectionStatusInfoObject { + QString status; //Attribute. The connection status. + QString deviceName; //Attribute. The name of the Bluetooth source. +}; + +struct NowPlayingObject { + QString deviceID; //Attribute. Unique identifier of the product. + QString source; //Attribute. The type or name of the service playing. To determine if the product is in standby mode, check if source == STANDBY. + QString sourceAccount; //Attribute. The account associated with this source. + ContentItemObject ContentItem; //Element. This object describes the content container that is playing on the product. You should treat this object as an opaque blob, for use in /select. + QString track; //Element. The track name. + QString artist; //Eement. The artist name. + QString album; //Element. The album name. + QString genre; //Element. The music genre. + QString rating; //Element. The user rating of the song. + QString stationName; //Element. The station or playlist name. + ArtObject art; //Element. This object provides information for source content art. See art object + TimeObject time; //Element. This object provides information for current and total track time. See time object + bool skipEnabled; //Element. If tag is present, the /key value NEXT_TRACK is valid. + bool skipPreviousEnabled; //Element. If tag is present, the /key value PREV_TRACK is valid. + bool favoriteEnabled; //Element. If tag is present, the /key values ADD_FAVORITE and REMOVE_FAVORITE are valid. + bool isFavorit; //Element. If tag is present, the track or station has been favorited by the user. + bool rateEnabled; //Element. If tag is present, the track can be rated by the user using /key THUMBS_UP and THUMBS_DOWN. + //QString rating; //Element. UP, DOWN, or NONE rating. If tag isn't present, the track has not been rated by the user. + PLAY_STATUS playStatus; //Element. This indicates whether the audio product is currently playing, paused, in standby, or in some other state. + SHUFFLE_STATUS shuffleSetting; //Element. The shuffle state. If this tag is present, the /key values SHUFFLE_ON and SHUFFLE_OFF are valid. + REPEAT_STATUS repeatSettings; //Element. The repeat state. If this tag is present, the /key values REPEAT_OFF, REPEAT_ONE, and REPEAT_ALL are valid. + STREAM_STATUS streamType; //Element. The type of music that is streaming. + QString stationLocation; //Element. The location of the source ex: "Internet Only". + ConnectionStatusInfoObject ConnectionStatusInfo; //Element. This object describes the connection status to the Bluetooth source. See ConnectionStatusInfo object. +}; + +struct SourceItemObject { + QString source; //Attribute. The name of the source. + QString sourceAccount; //Attribute. The user account associated with the source. + SOURCE_STATUS status; //Attribute. Indicates whether the source is available. + bool isLocal; //Attribute. TRUE if a local source (AUX or BLUETOOTH) + bool multiroomallowed; //Attribute. TRUE if the source can be rebroadcast in a multi-room zone. + QString displayName; //Content. The user-facing name of the source. +}; + +struct SourcesObject { + QString deviceId; + SourceItemObject sourceItem; +}; + +struct VolumeObject { + QString deviceID; //Attribute. Unique identifier of the product. + int targetVolume; //Element. The volume the product is trying to reach, 0 to 100 inclusive. Bigger is louder. + int actualVolume; //Element. The current product volume, 0 to 100 inclusive. Bigger is louder. + bool muteEnabled; //Element. TRUE if the product is muted. +}; + +struct MemberObject { + QString ipAddress; //Attribute. The IP address of the product. + QString deviceID; //The deviceID unique identifier of the product. +}; + +struct ZoneObject { + QString deviceID; //Attribute. The deviceID unique identifier of the master product. + MemberObject member; //Element. This object describes products in the zone There is an object for each product. +}; + +struct BassCapabilitiesObject { + QString deviceID; //Attribute. Unique identifier of the product. + bool bassAvailable; //Element. TRUE if the bass of the advice is adjustable. + int bassMin; //Element. The minimum value the bass can be set to. + int bassMax; //Element. The maximum value the bass can be set to. + int bassDefault; //Element. The default bass value. +}; + +struct BassObject { + QString deviceID; //Attribute. Unique identifier of the product. + int targetBass; //Element. The bass level the product is trying to reach. + int actualBass; //Element. The current bass level. +}; + +struct PresetObject { + int presetId; //Attribute. The number of the SoundTouch preset, 1-6 inclusive. + uint64_t createdOn; //Attribute. A timestamp of when the preset was originally created. + uint64_t updatedOn; //Attribute. A timestamp of the last time the preset was changed. + ContentItemObject ContentItem; //Attribute. An object describing the source. See /now_playing for full documentation on this object. +}; + +struct GroupRoleObject { + QString deviceID; //Unique identifier of the product. + GROUP_LOC role; //The user-set location of the product. LEFT or RIGHT. + QString ipAddress; //The IP Address of the product. +}; + +struct RolesObject { + GroupRoleObject groupRole; //This object describes a product in the group. +}; + +struct GroupObject { + QString id; //Attribute. A unique ID for the group. + QString name; //Element. A user-set name for the group. + QString masterDeviceId; //Element. The unique identifier of the master product in the group. + RolesObject roles; //Element. This object describes the products in the group and their location (left/right). + PLAY_STATUS status; //Element. The state of the stereo pair group. +}; + +#endif // SOUNDTOUCHTYPES_H + diff --git a/nymea-plugins.pro b/nymea-plugins.pro index 8985227c..7c33d9b7 100644 --- a/nymea-plugins.pro +++ b/nymea-plugins.pro @@ -5,6 +5,7 @@ PLUGIN_DIRS = \ avahimonitor \ awattar \ boblight \ + bose \ commandlauncher \ conrad \ datetime \ From dbef0ff2023368509140700fceb95d4e70ff0651 Mon Sep 17 00:00:00 2001 From: nymea Date: Thu, 11 Jul 2019 12:25:39 +0200 Subject: [PATCH 2/9] actions and states are now fixed --- bose/soundtouch.cpp | 382 +++++++++++++++++++++++++++++++++++++---- bose/soundtouch.h | 50 ++++-- bose/soundtouchtypes.h | 32 +++- 3 files changed, 421 insertions(+), 43 deletions(-) diff --git a/bose/soundtouch.cpp b/bose/soundtouch.cpp index 9af989bf..11812643 100644 --- a/bose/soundtouch.cpp +++ b/bose/soundtouch.cpp @@ -1,3 +1,25 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * * + * Copyright (C) 2019 Bernhard Trinnes . * + * * + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + #include "soundtouch.h" #include "hardwaremanager.h" #include "devices/device.h" @@ -18,6 +40,7 @@ SoundTouch::SoundTouch(NetworkAccessManager *networkAccessManager, QString ipAdd url.setPort(8080); qDebug(dcBose) << "Connecting websocket to" << url; //TODO missing websocket subprotocol "gabbo" + //Seems QWebsockets doesn't support subprotocols m_websocket->open(url); } @@ -28,7 +51,7 @@ void SoundTouch::getInfo() url.setScheme("http"); url.setPort(m_port); url.setPath("/info"); - qDebug(dcBose) << "Sending request" << url; + //qDebug(dcBose) << "Sending request" << url; QNetworkReply *reply = m_networkAccessManager->get(QNetworkRequest(url)); connect(reply, &QNetworkReply::finished, this, &SoundTouch::onRestRequestFinished); } @@ -40,7 +63,7 @@ void SoundTouch::getVolume() url.setScheme("http"); url.setPort(m_port); url.setPath("/volume"); - qDebug(dcBose) << "Sending request" << url; + //qDebug(dcBose) << "Sending request" << url; QNetworkReply *reply = m_networkAccessManager->get(QNetworkRequest(url)); connect(reply, &QNetworkReply::finished, this, &SoundTouch::onRestRequestFinished); } @@ -52,7 +75,7 @@ void SoundTouch::getNowPlaying() url.setScheme("http"); url.setPort(m_port); url.setPath("/now_playing"); - qDebug(dcBose) << "Sending request" << url; + //qDebug(dcBose) << "Sending request" << url; QNetworkReply *reply = m_networkAccessManager->get(QNetworkRequest(url)); connect(reply, &QNetworkReply::finished, this, &SoundTouch::onRestRequestFinished); } @@ -64,7 +87,7 @@ void SoundTouch::getBass() url.setScheme("http"); url.setPort(m_port); url.setPath("/bass"); - qDebug(dcBose) << "Sending request" << url; + //qDebug(dcBose) << "Sending request" << url; QNetworkReply *reply = m_networkAccessManager->get(QNetworkRequest(url)); connect(reply, &QNetworkReply::finished, this, &SoundTouch::onRestRequestFinished); } @@ -83,12 +106,38 @@ void SoundTouch::getGroup() void SoundTouch::getSources() { - + QUrl url; + url.setHost(m_ipAddress); + url.setScheme("http"); + url.setPort(m_port); + url.setPath("/sources"); + //qDebug(dcBose) << "Sending request" << url; + QNetworkReply *reply = m_networkAccessManager->get(QNetworkRequest(url)); + connect(reply, &QNetworkReply::finished, this, &SoundTouch::onRestRequestFinished); } void SoundTouch::getZone() { + QUrl url; + url.setHost(m_ipAddress); + url.setScheme("http"); + url.setPort(m_port); + url.setPath("/getZone"); + qDebug(dcBose) << "Sending request" << url; + QNetworkReply *reply = m_networkAccessManager->get(QNetworkRequest(url)); + connect(reply, &QNetworkReply::finished, this, &SoundTouch::onRestRequestFinished); +} +void SoundTouch::getPresets() +{ + QUrl url; + url.setHost(m_ipAddress); + url.setScheme("http"); + url.setPort(m_port); + url.setPath("/presets"); + qDebug(dcBose) << "Sending request" << url; + QNetworkReply *reply = m_networkAccessManager->get(QNetworkRequest(url)); + connect(reply, &QNetworkReply::finished, this, &SoundTouch::onRestRequestFinished); } void SoundTouch::setKey(KEY_VALUE keyValue) @@ -107,6 +156,9 @@ void SoundTouch::setKey(KEY_VALUE keyValue) case KEY_VALUE_STOP: content.append("STOP"); break; + case KEY_VALUE_PAUSE: + content.append("PAUSE"); + break; case KEY_VALUE_PLAY_PAUSE: content.append("PLAY_PAUSE"); break; @@ -165,7 +217,7 @@ void SoundTouch::setKey(KEY_VALUE keyValue) content.append(""); content.append("POWER"); content.append(""); - qDebug(dcBose) << "Sending request" << url << content; + //qDebug(dcBose) << "Sending request" << url << content; QNetworkReply *reply = m_networkAccessManager->post(QNetworkRequest(url), content); connect(reply, &QNetworkReply::finished, this, &SoundTouch::onRestRequestFinished); } @@ -182,6 +234,106 @@ void SoundTouch::setVolume(int volume) content.append(""); content.append(QByteArray::number(volume)); content.append(""); + //qDebug(dcBose) << "Sending request" << url << content; + QNetworkReply *reply = m_networkAccessManager->post(QNetworkRequest(url), content); + connect(reply, &QNetworkReply::finished, this, &SoundTouch::onRestRequestFinished); +} + +void SoundTouch::setSource(ContentItemObject contentItem) +{ + QUrl url; + url.setHost(m_ipAddress); + url.setScheme("http"); + url.setPort(m_port); + url.setPath("/select"); //Select source + QByteArray content; + QXmlStreamWriter xml(&content); + xml.writeStartDocument(); + xml.writeStartElement("ContentItem"); + xml.writeAttribute("source", contentItem.source); + xml.writeAttribute("sourceAccount", contentItem.sourceAccount); + xml.writeEndElement(); //ContentItem + xml.writeEndDocument(); + qDebug(dcBose) << "Sending request" << url << content; + + //QNetworkReply *reply = m_networkAccessManager->post(QNetworkRequest(url), content); + //connect(reply, &QNetworkReply::finished, this, &SoundTouch::onRestRequestFinished); +} + +void SoundTouch::setZone(ZoneObject zone) +{ + QUrl url; + url.setHost(m_ipAddress); + url.setScheme("http"); + url.setPort(m_port); + url.setPath("/setZone"); + QByteArray content; + QXmlStreamWriter xml(&content); + xml.writeStartDocument(); + xml.writeStartElement("zone"); + xml.writeAttribute("master", zone.deviceID); + xml.writeTextElement("member", zone.member.deviceID); + xml.writeAttribute("ipaddress", zone.member.ipAddress); + xml.writeEndElement(); //zone + xml.writeEndDocument(); + qDebug(dcBose) << "Sending request" << url << content; + //QNetworkReply *reply = m_networkAccessManager->post(QNetworkRequest(url), content); + //connect(reply, &QNetworkReply::finished, this, &SoundTouch::onRestRequestFinished); +} + +void SoundTouch::addZoneSlave(ZoneObject zone) +{ + QUrl url; + url.setHost(m_ipAddress); + url.setScheme("http"); + url.setPort(m_port); + url.setPath("/addZoneSlave"); + QByteArray content; + QXmlStreamWriter xml(&content); + xml.writeStartDocument(); + xml.writeStartElement("zone"); + xml.writeAttribute("master", zone.deviceID); + xml.writeTextElement("member", zone.member.deviceID); + xml.writeAttribute("ipaddress", zone.member.ipAddress); + xml.writeEndElement(); //zone + xml.writeEndDocument(); + qDebug(dcBose) << "Sending request" << url << content; + //QNetworkReply *reply = m_networkAccessManager->post(QNetworkRequest(url), content); + //connect(reply, &QNetworkReply::finished, this, &SoundTouch::onRestRequestFinished); +} + +void SoundTouch::removeZoneSlave(ZoneObject zone) +{ + QUrl url; + url.setHost(m_ipAddress); + url.setScheme("http"); + url.setPort(m_port); + url.setPath("/removeZoneSlave"); + QByteArray content; + QXmlStreamWriter xml(&content); + xml.writeStartDocument(); + xml.writeStartElement("zone"); + xml.writeAttribute("master", zone.deviceID); + xml.writeTextElement("member", zone.member.deviceID); + xml.writeAttribute("ipaddress", zone.member.ipAddress); + xml.writeEndElement(); //zone + xml.writeEndDocument(); + qDebug(dcBose) << "Sending request" << url << content; + //QNetworkReply *reply = m_networkAccessManager->post(QNetworkRequest(url), content); + //connect(reply, &QNetworkReply::finished, this, &SoundTouch::onRestRequestFinished); +} + +void SoundTouch::setBass(int volume) +{ + QUrl url; + url.setHost(m_ipAddress); + url.setScheme("http"); + url.setPort(m_port); + url.setPath("/bass"); + QByteArray content = (""); + content.append(""); + content.append(QByteArray::number(volume)); + content.append(""); qDebug(dcBose) << "Sending request" << url << content; QNetworkReply *reply = m_networkAccessManager->post(QNetworkRequest(url), content); connect(reply, &QNetworkReply::finished, this, &SoundTouch::onRestRequestFinished); @@ -203,6 +355,30 @@ void SoundTouch::setName(QString name) connect(reply, &QNetworkReply::finished, this, &SoundTouch::onRestRequestFinished); } +void SoundTouch::setSpeaker(PlayInfoObject playInfo) +{ + QUrl url; + url.setHost(m_ipAddress); + url.setScheme("http"); + url.setPort(m_port); + url.setPath("/speaker"); + QByteArray content; + QXmlStreamWriter xml(&content); + xml.writeStartDocument(); + xml.writeStartElement("play_info"); + xml.writeTextElement("app_key", playInfo.appKey); + xml.writeTextElement("url", playInfo.url); + xml.writeTextElement("service", playInfo.services); + xml.writeTextElement("reason", playInfo.reason); + xml.writeTextElement("message", playInfo.message); + xml.writeTextElement("volume", QString::number(playInfo.volume)); + xml.writeEndElement(); //play_info + xml.writeEndDocument(); + qDebug(dcBose) << "Sending request" << url << content; + //QNetworkReply *reply = m_networkAccessManager->post(QNetworkRequest(url), content); + //connect(reply, &QNetworkReply::finished, this, &SoundTouch::onRestRequestFinished); +} + void SoundTouch::onWebsocketConnected() { qDebug(dcBose) << "Bose websocket connected"; @@ -229,9 +405,9 @@ void SoundTouch::onRestRequestFinished() { reply->deleteLater(); QByteArray data = reply->readAll(); - qDebug(dcBose) << "REST message received:" << data; int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); + //qDebug(dcBose) << data; // Check HTTP status code if (status != 200 || reply->error() != QNetworkReply::NoError) { qCWarning(dcBose()) << "Request error:" << status << reply->errorString(); @@ -250,24 +426,28 @@ void SoundTouch::onRestRequestFinished() { } while(xml.readNextStartElement()){ if(xml.name() == "name"){ - qDebug(dcBose) << "name" << xml.readElementText(); + //qDebug(dcBose) << "name" << xml.readElementText(); info.name = xml.readElementText(); } else if(xml.name() == "type"){ - qDebug(dcBose) << "type" << xml.readElementText(); + //qDebug(dcBose) << "type" << xml.readElementText(); info.type = xml.readElementText(); } else if(xml.name() == "components"){ - qDebug(dcBose) << "components element"; + //qDebug(dcBose) << "components element"; while(xml.readNextStartElement()){ if(xml.name() == "component"){ + ComponentObject component; while(xml.readNextStartElement()){ if(xml.name() == "softwareVersion"){ - qDebug(dcBose) << "Software version" << xml.readElementText(); + //qDebug(dcBose) << "Software version" << xml.readElementText(); + component.softwareVersion = xml.readElementText(); } else if(xml.name() == "serialNumber") { - qDebug(dcBose) << "Serialnumber" << xml.readElementText(); + //qDebug(dcBose) << "Serialnumber" << xml.readElementText(); + component.serialNumber = xml.readElementText(); } else { xml.skipCurrentElement(); } } + info.components.append(component); } else { xml.skipCurrentElement(); } @@ -288,27 +468,171 @@ void SoundTouch::onRestRequestFinished() { } } emit infoReceived(info); - } else if (xml.name() == "now_playing") { + } else if (xml.name() == "nowPlaying") { NowPlayingObject nowPlaying; + if(xml.attributes().hasAttribute("deviceID")) { + //qDebug(dcBose) << "Device ID" << xml.attributes().value("deviceID").toString(); + nowPlaying.deviceID = xml.attributes().value("deviceID").toString(); + } + if(xml.attributes().hasAttribute("source")) { + //qDebug(dcBose) << "Source" << xml.attributes().value("source").toString(); + nowPlaying.source = xml.attributes().value("source").toString(); + } + if(xml.attributes().hasAttribute("sourceAccount")) { + //qDebug(dcBose) << "Source Account" << xml.attributes().value("sourceAccount").toString(); + nowPlaying.sourceAccount = xml.attributes().value("sourceAccount").toString(); + } + while(xml.readNextStartElement()){ + if (xml.name() == "track") { + //qDebug(dcBose) << "track" << xml.readElementText(); + nowPlaying.track = xml.readElementText(); + } else if(xml.name() == "artist") { + //qDebug(dcBose) << "artist" << xml.readElementText(); + nowPlaying.artist = xml.readElementText(); + } else if(xml.name() == "album") { + //qDebug(dcBose) << "album" << xml.readElementText(); + nowPlaying.album = xml.readElementText(); + } else if(xml.name() == "genre") { + //qDebug(dcBose) << "genre" << xml.readElementText(); + nowPlaying.genre = xml.readElementText(); + } else if(xml.name() == "rating") { + //qDebug(dcBose) << "rating" << xml.readElementText(); + nowPlaying.rating = xml.readElementText(); + } else if(xml.name() == "stationName") { + //qDebug(dcBose) << "Station name" << xml.readElementText(); + nowPlaying.stationName = xml.readElementText(); + } else if(xml.name() == "art") { + ArtObject art; + if(xml.attributes().hasAttribute("artImageStatus")) { + QString artStatus = xml.attributes().value("artImageStatus").toString().toUpper(); + //ART_STATUS: INVALID, SHOW_DEFAULT_IMAGE, DOWNLOADING, IMAGE_PRESENT + //qDebug(dcBose) << "Art Image status" << artStatus; + if (artStatus == "INVALID") { + art.artStatus = ART_STATUS_INVALID; + } else if (artStatus == "SHOW_DEFAULT_IMAGE") { + art.artStatus = ART_STATUS_SHOW_DEFAULT_IMAGE; + } else if (artStatus == "DOWNLOADING") { + art.artStatus = ART_STATUS_DOWNLOADING; + } else if (artStatus == "IMAGE_PRESENT") { + art.artStatus = ART_STATUS_IMAGE_PRESENT; + } + } + nowPlaying.art.url = xml.readElementText(); + }else if(xml.name() == "playStatus") { + QString playStatus = xml.readElementText(); + //qDebug(dcBose) << "Play Status" << playStatus; + //Modes: PLAY_STATE, PAUSE_STATE, STOP_STATE, BUFFERING_STATE + if (playStatus == "PLAY_STATE") { + nowPlaying.playStatus = PLAY_STATUS_PLAY_STATE; + } else if (playStatus == "PAUSE_STATE") { + nowPlaying.playStatus = PLAY_STATUS_PAUSE_STATE; + } else if (playStatus == "STOP_STATE") { + nowPlaying.playStatus = PLAY_STATUS_STOP_STATE; + } else if (playStatus == "BUFFERING_STATE") { + nowPlaying.playStatus = PLAY_STATUS_BUFFERING_STATE; + } + } else if(xml.name() == "shuffleSetting") { + QString shuffle = xml.readElementText().toUpper(); + //qDebug(dcBose) << "Shuffle Setting" << shuffle; + if (shuffle == "SHUFFLE_ON") { + nowPlaying.shuffleSetting = SHUFFLE_STATUS_SHUFFLE_ON; + } else { + nowPlaying.shuffleSetting = SHUFFLE_STATUS_SHUFFLE_OFF; + } + }else if(xml.name() == "repeatSetting") { + QString repeat = xml.readElementText().toUpper(); + //qDebug(dcBose) << "Repeat Setting" << repeat; + //Modes: REPEAT_OFF, REPEAT_ALL, REPEAT_ONE + if (repeat == "REPEAT_OFF") { + nowPlaying.repeatSettings = REPEAT_STATUS_REPEAT_OFF; + } else if (repeat == "REPEAT_ONE") { + nowPlaying.repeatSettings = REPEAT_STATUS_REPEAT_ONE; + } else if (repeat == "REPEAT_ALL") { + nowPlaying.repeatSettings = REPEAT_STATUS_REPEAT_ALL; + } + } else if(xml.name() == "streamType") { + QString streamType = xml.readElementText().toUpper(); + //qDebug(dcBose) << "Stream Type" << streamType; + //Types: TRACK_ONDEMAND, RADIO_STREAMING, RADIO_TRACKS, NO_TRANSPORT_CONTROLS + if (streamType == "RADIO_TRACKS") { + nowPlaying.streamType = STREAM_STATUS_RADIO_TRACKS; + } else if (streamType == "TRACK_ONDEMAND") { + nowPlaying.streamType = STREAM_STATUS_TRACK_ONDEMAND; + } else if (streamType == "RADIO_STREAMING") { + nowPlaying.streamType = STREAM_STATUS_RADIO_STREAMING; + } else if (streamType == "NO_TRANSPORT_CONTROLS") { + nowPlaying.streamType = STREAM_STATUS_NO_TRANSPORT_CONTROLS; + }; + } else if(xml.name() == "stationLocation") { + nowPlaying.stationLocation = xml.readElementText(); + } else { + xml.skipCurrentElement(); + } + } - emit + emit nowPlayingReceived(nowPlaying); + } else if (xml.name() == "volume") { + VolumeObject volumeObject; + if(xml.attributes().hasAttribute("deviceID")) { + //qDebug(dcBose) << "Device ID" << xml.attributes().value("deviceID").toString(); + volumeObject.deviceID = xml.attributes().value("deviceID").toString(); + } + while(xml.readNextStartElement()){ + if(xml.name() == "targetvolume"){ + //qDebug(dcBose) << "Target volume" << xml.readElementText(); + volumeObject.targetVolume = xml.readElementText().toInt(); + }else if(xml.name() == "actualvolume"){ + //qDebug(dcBose) << "Actual volume" << xml.readElementText(); + volumeObject.actualVolume = xml.readElementText().toInt(); + }else if(xml.name() == "muteenabled"){ + //qDebug(dcBose) << "Mute enabled" << xml.readElementText(); + volumeObject.muteEnabled = ( xml.readElementText().toUpper() == "TRUE" ); //TODO convert from "false" to bool + }else { + xml.skipCurrentElement(); + } + } + emit volumeReceived(volumeObject); + } else if (xml.name() == "sources") { + SourcesObject sourcesObject; + if(xml.attributes().hasAttribute("deviceID")) { + //qDebug(dcBose) << "Device ID" << xml.attributes().value("deviceID").toString(); + sourcesObject.deviceId = xml.attributes().value("deviceID").toString(); + } + while(xml.readNextStartElement()){ + if(xml.name() == "sourceItem"){ + SourceItemObject sourceItem; + if(xml.attributes().hasAttribute("source")) { + //qDebug(dcBose) << "Source" << xml.attributes().value("source").toString(); + sourceItem.source = xml.attributes().value("source").toString(); + } else if(xml.attributes().hasAttribute("sourceAccount")) { + //qDebug(dcBose) << "Source Account" << xml.attributes().value("sourceAccount").toString(); + sourceItem.sourceAccount = xml.attributes().value("sourceAccount").toString(); + } else if(xml.attributes().hasAttribute("status")) { + QString status = xml.attributes().value("status").toString().toUpper(); //UNAVAILABLE, READY + //qDebug(dcBose) << "status" << status; + if (status == "READY") { + sourceItem.status = SOURCE_STATUS_READY; + } else { + sourceItem.status = SOURCE_STATUS_UNAVAILABLE; + } + } else if(xml.attributes().hasAttribute("isLocal")) { + //qDebug(dcBose) << "is Local" << xml.attributes().value("isLocal").toString(); + sourceItem.isLocal = ( xml.attributes().value("isLocal").toString().toUpper() == "TRUE" ); + } else if(xml.attributes().hasAttribute("multiroomallowed")) { + //Debug(dcBose) << "multiroom allowed" << xml.attributes().value("multiroomallowed").toString(); + sourceItem.multiroomallowed = ( xml.attributes().value("multiroomallowed").toString().toUpper() == "TRUE" ); + } + sourceItem.displayName = xml.readElementText(); + sourcesObject.sourceItems.append(sourceItem); + }else { + xml.skipCurrentElement(); + } + } + emit sourcesReceived(sourcesObject); + } else { + xml.skipCurrentElement(); } } - - /*while (!xml.atEnd()) { - if (xml.readNext()) { - if (xml.tokenType() == TokenType::) - qDebug(dcBose) << "element" << xml.tokenString() << xml.readElementText(); - } - - if (xml.hasError()) { - qCWarning(dcBose()) << "Error when parsing XML response"; - } - }*/ - - - //} - } diff --git a/bose/soundtouch.h b/bose/soundtouch.h index e9416cec..69111cbc 100644 --- a/bose/soundtouch.h +++ b/bose/soundtouch.h @@ -1,3 +1,25 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * * + * Copyright (C) 2019 Bernhard Trinnes . * + * * + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + #ifndef SOUNDTOUCH_H #define SOUNDTOUCH_H @@ -23,29 +45,31 @@ public: void getNowPlaying(); //Get information about what's playing on a product. void getBass(); //Get the bass level of a product, if supported. void getGroup(); //Get the current left/right stereo pair configuration of a product. - void getSources(); //Get available sources for a product. + void getSources(); //Get available sources for a product. void getZone(); //Get the current multiroom zone state of a product. - //void getPresets(); //Get information related to the user's SoundTouch presets. - //void getBassCapabilities(); //Get whether a product supports reducing bass. - //void speaker(); //initiate playback of a specified network-accessible audio file on a Bose SoundTouch product. - void setKey(KEY_VALUE keyValue); //Start and control playback on a product. - void setVolume(int volume); //Set the volume of a product. - void setSource(SOURCE_STATUS source); //Select a product's source. - /*void setZone(QString zone); //Create a zone of synced products. - void addZoneSlave(); //Add one or more slave product(s) to a multiroom zone. - void removeZoneSlave(); //Remove one or more slave product(s) from a multiroom zone. - void setBass(int volume); //Set the bass level of a product, if supported.*/ - void setName(QString name); //Set the products user-facing name. + void getPresets(); //Get information related to the user's SoundTouch presets. + void getBassCapabilities(); //Get whether a product supports reducing bass. + + void setKey(KEY_VALUE keyValue); //Start and control playback on a product. + void setVolume(int volume); //Set the volume of a product. + void setSource(ContentItemObject contentItem); //Select a product's source. + void setZone(ZoneObject zone); //Create a zone of synced products. + void addZoneSlave(ZoneObject zone); //Add one or more slave product(s) to a multiroom zone. + void removeZoneSlave(ZoneObject zone); //Remove one or more slave product(s) from a multiroom zone. + void setBass(int volume); //Set the bass level of a product, if supported.*/ + void setName(QString name); //Set the products user-facing name. + void setSpeaker(PlayInfoObject playInfo); //initiate playback of a specified network-accessible audio file on a Bose SoundTouch product. + private: NetworkAccessManager *m_networkAccessManager = nullptr; QString m_ipAddress; int m_port = 8090; QWebSocket *m_websocket = nullptr; - //QHash m_requests; signals: void connectionChanged(bool connected); + void infoReceived(InfoObject info); void nowPlayingReceived(NowPlayingObject nowPlaying); void volumeReceived(VolumeObject volume); diff --git a/bose/soundtouchtypes.h b/bose/soundtouchtypes.h index d4724d40..4ecac7e1 100644 --- a/bose/soundtouchtypes.h +++ b/bose/soundtouchtypes.h @@ -1,3 +1,25 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * * + * Copyright (C) 2019 Bernhard Trinnes . * + * * + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + #ifndef SOUNDTOUCHTYPES_H #define SOUNDTOUCHTYPES_H @@ -157,7 +179,7 @@ struct SourceItemObject { struct SourcesObject { QString deviceId; - SourceItemObject sourceItem; + QList sourceItems; }; struct VolumeObject { @@ -216,5 +238,13 @@ struct GroupObject { PLAY_STATUS status; //Element. The state of the stereo pair group. }; +struct PlayInfoObject { + QString appKey; //Element. An authorization key used to identify the client application. Apply for an app key by creating an app here. + QString url; //Element. A fully qualified, web hosted stream URL. The URL should include the 'http://' prefix as well as a stream suffix ('mp3', ...) for proper playback. + QString services; //Element. This indicates the service providing the notification. This text will appear on the device display (when available) and the SoundTouch application screen. + QString reason; //Element. This indicates the reason for the notification. This text will appear on the device display (when available) and the SoundTouch application screen. If a reason string is not provided, the field with be blank. + QString message; //Element. This indicates further details about the notification. This text will appear on the device display (when available) and the SoundTouch application screen. If a message string is not provided, the field with be blank. + int volume; //Element. This indicates the desired volume level while playing the notification. The value represents a percentage (0 to 100) of the full audible range of the speaker device. A value less than 10 or greater than 70 will result in an error and not play the notification. Upon completion of the notification, the speaker volume will return to its original value. If not present, the notification will play at the existing volume level. +}; #endif // SOUNDTOUCHTYPES_H From 73caf35616ff7344af5361d4041f265dc24d6373 Mon Sep 17 00:00:00 2001 From: nymea Date: Thu, 11 Jul 2019 12:37:35 +0200 Subject: [PATCH 3/9] removed some more debug prints --- bose/soundtouch.cpp | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/bose/soundtouch.cpp b/bose/soundtouch.cpp index 11812643..6738ccf2 100644 --- a/bose/soundtouch.cpp +++ b/bose/soundtouch.cpp @@ -419,9 +419,8 @@ void SoundTouch::onRestRequestFinished() { if (xml.readNextStartElement()) { if (xml.name() == "info") { InfoObject info; - qDebug(dcBose) << "Info Request"; if(xml.attributes().hasAttribute("deviceID")) { - qDebug(dcBose) << "Device ID" << xml.attributes().value("deviceID").toString(); + //qDebug(dcBose) << "Device ID" << xml.attributes().value("deviceID").toString(); info.deviceID = xml.attributes().value("deviceID").toString(); } while(xml.readNextStartElement()){ @@ -453,12 +452,11 @@ void SoundTouch::onRestRequestFinished() { } } } else if(xml.name() == "networkInfo"){ - qDebug(dcBose) << "network Info"; while (xml.readNextStartElement()) { if (xml.name() == "macAddress") { - qDebug(dcBose) << "macAddress" << xml.readElementText(); + info.networkInfo.macAddress = xml.readElementText(); } else if(xml.name() == "ipAddress") { - qDebug(dcBose) << "ipAddress" << xml.readElementText(); + info.networkInfo.ipAddress = xml.readElementText(); } else { xml.skipCurrentElement(); } From 5498905a499b98adf7890a1cc4cc1c21a3971356 Mon Sep 17 00:00:00 2001 From: nymea Date: Thu, 11 Jul 2019 12:54:31 +0200 Subject: [PATCH 4/9] added missing files --- bose/bose.pro | 16 +++ bose/devicepluginbose.cpp | 277 +++++++++++++++++++++++++++++++++++++ bose/devicepluginbose.h | 67 +++++++++ bose/devicepluginbose.json | 187 +++++++++++++++++++++++++ 4 files changed, 547 insertions(+) create mode 100644 bose/bose.pro create mode 100644 bose/devicepluginbose.cpp create mode 100644 bose/devicepluginbose.h create mode 100644 bose/devicepluginbose.json diff --git a/bose/bose.pro b/bose/bose.pro new file mode 100644 index 00000000..a168666e --- /dev/null +++ b/bose/bose.pro @@ -0,0 +1,16 @@ +include(../plugins.pri) + +QT += \ + network \ + websockets \ + +TARGET = $$qtLibraryTarget(nymea_devicepluginbose) + +SOURCES += \ + devicepluginbose.cpp \ + soundtouch.cpp + +HEADERS += \ + devicepluginbose.h \ + soundtouch.h \ + soundtouchtypes.h diff --git a/bose/devicepluginbose.cpp b/bose/devicepluginbose.cpp new file mode 100644 index 00000000..e484e31c --- /dev/null +++ b/bose/devicepluginbose.cpp @@ -0,0 +1,277 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * * + * Copyright (C) 2019 Bernhard Trinnes . * + * * + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +#include "devicepluginbose.h" +#include "devices/device.h" +#include "plugininfo.h" +#include "platform/platformzeroconfcontroller.h" +#include "network/zeroconf/zeroconfservicebrowser.h" +#include "network/zeroconf/zeroconfserviceentry.h" + +#include +#include + +DevicePluginBose::DevicePluginBose() +{ +} + +DevicePluginBose::~DevicePluginBose() +{ +} + +void DevicePluginBose::init() +{ +} + +Device::DeviceSetupStatus DevicePluginBose::setupDevice(Device *device) +{ + if (!m_pluginTimer) { + m_pluginTimer = hardwareManager()->pluginTimerManager()->registerTimer(2); + connect(m_pluginTimer, &PluginTimer::timeout, this, &DevicePluginBose::onPluginTimer); + } + + if (device->deviceClassId() == soundtouchDeviceClassId) { + + connect(device, &Device::nameChanged, this, &DevicePluginBose::onDeviceNameChanged); + + QString ipAddress = device->paramValue(soundtouchDeviceIpParamTypeId).toString(); + SoundTouch *soundTouch = new SoundTouch(hardwareManager()->networkManager(), ipAddress, this); + connect(soundTouch, &SoundTouch::connectionChanged, this, &DevicePluginBose::onConnectionChanged); + + connect(soundTouch, &SoundTouch::infoReceived, this, &DevicePluginBose::onInfoObjectReceived); + connect(soundTouch, &SoundTouch::nowPlayingReceived, this, &DevicePluginBose::onNowPlayingObjectReceived); + connect(soundTouch, &SoundTouch::volumeReceived, this, &DevicePluginBose::onVolumeObjectReceived); + connect(soundTouch, &SoundTouch::sourcesReceived, this, &DevicePluginBose::onSourcesObjectReceived); + + soundTouch->getInfo(); + soundTouch->getNowPlaying(); + soundTouch->getVolume(); + soundTouch->getSources(); + + m_soundTouch.insert(device, soundTouch); + + return Device::DeviceSetupStatusSuccess; + } + return Device::DeviceSetupStatusFailure; +} + +void DevicePluginBose::deviceRemoved(Device *device) +{ + if (device->deviceClassId() == soundtouchDeviceClassId) { + SoundTouch *soundTouch = m_soundTouch.value(device); + soundTouch->deleteLater(); + } + + if (m_pluginTimer && myDevices().isEmpty()) { + hardwareManager()->pluginTimerManager()->unregisterTimer(m_pluginTimer); + } +} + +Device::DeviceError DevicePluginBose::discoverDevices(const DeviceClassId &deviceClassId, const ParamList ¶ms) +{ + Q_UNUSED(params) + Q_UNUSED(deviceClassId) + + ZeroConfServiceBrowser *serviceBrowser = hardwareManager()->zeroConfController()->createServiceBrowser("_soundtouch._tcp"); + QTimer::singleShot(5000, this, [this, serviceBrowser](){ + QList descriptors; + foreach (const ZeroConfServiceEntry avahiEntry, serviceBrowser->serviceEntries()) { + qCDebug(dcBose) << "Zeroconf entry:" << avahiEntry; + + QString playerId = avahiEntry.hostName().split(".").first(); + DeviceDescriptor descriptor(soundtouchDeviceClassId, avahiEntry.name(), avahiEntry.hostAddress().toString()); + ParamList params; + params << Param(soundtouchDeviceIpParamTypeId, avahiEntry.hostAddress().toString()); + params << Param(soundtouchDevicePlayerIdParamTypeId, playerId); + descriptor.setParams(params); + descriptors << descriptor; + } + emit devicesDiscovered(soundtouchDeviceClassId, descriptors); + serviceBrowser->deleteLater(); + }); + + return Device::DeviceErrorAsync; +} + +Device::DeviceError DevicePluginBose::executeAction(Device *device, const Action &action) +{ + if (device->deviceClassId() == soundtouchDeviceClassId) { + SoundTouch *soundTouch = m_soundTouch.value(device); + + if (action.actionTypeId() == soundtouchPowerActionTypeId) { + //bool power = action.param(soundtouchPowerActionPowerParamTypeId).value().toBool(); + soundTouch->setKey(KEY_VALUE::KEY_VALUE_POWER); //only toggling possible + return Device::DeviceErrorNoError; + } + if (action.actionTypeId() == soundtouchMuteActionTypeId) { + soundTouch->setKey(KEY_VALUE::KEY_VALUE_MUTE); + return Device::DeviceErrorNoError; + } + if (action.actionTypeId() == soundtouchPlayActionTypeId) { + soundTouch->setKey(KEY_VALUE::KEY_VALUE_PLAY); + return Device::DeviceErrorNoError; + } + if (action.actionTypeId() == soundtouchPauseActionTypeId) { + soundTouch->setKey(KEY_VALUE::KEY_VALUE_PAUSE); + return Device::DeviceErrorNoError; + } + if (action.actionTypeId() == soundtouchStopActionTypeId) { + soundTouch->setKey(KEY_VALUE::KEY_VALUE_STOP); + return Device::DeviceErrorNoError; + } + if (action.actionTypeId() == soundtouchSkipNextActionTypeId) { + soundTouch->setKey(KEY_VALUE::KEY_VALUE_NEXT_TRACK); + return Device::DeviceErrorNoError; + } + if (action.actionTypeId() == soundtouchSkipBackActionTypeId) { + soundTouch->setKey(KEY_VALUE::KEY_VALUE_PREV_TRACK); + return Device::DeviceErrorNoError; + } + + if (action.actionTypeId() == soundtouchShuffleActionTypeId) { + + bool shuffle = action.param(soundtouchShuffleActionShuffleParamTypeId).value().toBool(); + if (shuffle) { + soundTouch->setKey(KEY_VALUE::KEY_VALUE_SHUFFLE_ON); + } else { + soundTouch->setKey(KEY_VALUE::KEY_VALUE_SHUFFLE_OFF); + } + return Device::DeviceErrorNoError; + } + + if (action.actionTypeId() == soundtouchRepeatActionTypeId) { + + QString repeat = action.param(soundtouchRepeatActionRepeatParamTypeId).value().toString(); + if (repeat == "None") { + soundTouch->setKey(KEY_VALUE::KEY_VALUE_REPEAT_OFF); + } else if (repeat == "One") { + soundTouch->setKey(KEY_VALUE::KEY_VALUE_REPEAT_ONE); + } else if (repeat == "All") { + soundTouch->setKey(KEY_VALUE::KEY_VALUE_REPEAT_ALL); + } + return Device::DeviceErrorNoError; + } + + if (action.actionTypeId() == soundtouchVolumeActionTypeId) { + int volume = action.param(soundtouchVolumeActionVolumeParamTypeId).value().toInt(); + soundTouch->setVolume(volume); + return Device::DeviceErrorNoError; + } + + if (action.actionTypeId() == soundtouchPlaybackStatusActionTypeId) { + QString status = action.param(soundtouchPlaybackStatusActionPlaybackStatusParamTypeId).value().toString(); + if (status == "Playing") { + soundTouch->setKey(KEY_VALUE::KEY_VALUE_PLAY); + } else if (status == "Paused") { + soundTouch->setKey(KEY_VALUE::KEY_VALUE_PAUSE); + } else if (status == "Stopped") { + soundTouch->setKey(KEY_VALUE::KEY_VALUE_STOP); + } + return Device::DeviceErrorNoError; + } + + return Device::DeviceErrorActionTypeNotFound; + } + return Device::DeviceErrorDeviceClassNotFound; +} + +void DevicePluginBose::onPluginTimer() +{ + foreach(SoundTouch *soundTouch, m_soundTouch.values()) { + soundTouch->getInfo(); + soundTouch->getNowPlaying(); + soundTouch->getVolume(); + } +} + +void DevicePluginBose::onConnectionChanged(bool connected) +{ + SoundTouch *soundtouch = static_cast(sender()); + Device *device = m_soundTouch.key(soundtouch); + device->setStateValue(soundtouchConnectedStateTypeId, connected); +} + +void DevicePluginBose::onDeviceNameChanged() +{ + Device *device = static_cast(sender()); + SoundTouch *soundtouch = m_soundTouch.value(device); + soundtouch->setName(device->name()); +} + +void DevicePluginBose::onInfoObjectReceived(InfoObject infoObject) +{ + SoundTouch *soundtouch = static_cast(sender()); + Device *device = m_soundTouch.key(soundtouch); + device->setName(infoObject.name); +} + +void DevicePluginBose::onNowPlayingObjectReceived(NowPlayingObject nowPlaying) +{ + SoundTouch *soundtouch = static_cast(sender()); + Device *device = m_soundTouch.key(soundtouch); + + device->setStateValue(soundtouchTitleStateTypeId, nowPlaying.track); + device->setStateValue(soundtouchArtistStateTypeId, nowPlaying.artist); + device->setStateValue(soundtouchCollectionStateTypeId, nowPlaying.album); + device->setStateValue(soundtouchArtworkStateTypeId, nowPlaying.art.url); + device->setStateValue(soundtouchShuffleStateTypeId, ( nowPlaying.shuffleSetting == SHUFFLE_STATUS_SHUFFLE_ON )); + + switch (nowPlaying.repeatSettings) { + case REPEAT_STATUS_REPEAT_ONE: + device->setStateValue(soundtouchRepeatStateTypeId, "One"); + break; + case REPEAT_STATUS_REPEAT_ALL: + device->setStateValue(soundtouchRepeatStateTypeId, "All"); + break; + case REPEAT_STATUS_REPEAT_OFF: + device->setStateValue(soundtouchRepeatStateTypeId, "None"); + break; + } + + switch (nowPlaying.playStatus) { + case PLAY_STATUS_PLAY_STATE: + device->setStateValue(soundtouchPlaybackStatusStateTypeId, "Playing"); + break; + case PLAY_STATUS_PAUSE_STATE: + case PLAY_STATUS_BUFFERING_STATE: + device->setStateValue(soundtouchPlaybackStatusStateTypeId, "Paused"); + break; + case PLAY_STATUS_STOP_STATE: + device->setStateValue(soundtouchPlaybackStatusStateTypeId, "Stopped"); + break; + } +} + +void DevicePluginBose::onVolumeObjectReceived(VolumeObject volume) +{ + SoundTouch *soundtouch = static_cast(sender()); + Device *device = m_soundTouch.key(soundtouch); + device->setStateValue(soundtouchVolumeStateTypeId, volume.actualVolume); + device->setStateValue(soundtouchMuteStateTypeId, volume.muteEnabled); +} + +void DevicePluginBose::onSourcesObjectReceived(SourcesObject sources) +{ + foreach (SourceItemObject sourceItem, sources.sourceItems) { + qDebug(dcBose()) << "Source:" << sources.deviceId << sourceItem.source << sourceItem.displayName; + } +} diff --git a/bose/devicepluginbose.h b/bose/devicepluginbose.h new file mode 100644 index 00000000..382033a6 --- /dev/null +++ b/bose/devicepluginbose.h @@ -0,0 +1,67 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * * + * Copyright (C) 2019 Bernhard Trinnes . * + * * + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +#ifndef DEVICEPLUGINBOSE_H +#define DEVICEPLUGINBOSE_H + +#include "devices/deviceplugin.h" +#include "plugintimer.h" +#include "soundtouch.h" +#include "soundtouchtypes.h" + +#include +#include + +class DevicePluginBose : public DevicePlugin +{ + Q_OBJECT + Q_PLUGIN_METADATA(IID "io.nymea.DevicePlugin" FILE "devicepluginbose.json") + Q_INTERFACES(DevicePlugin) + +public: + explicit DevicePluginBose(); + ~DevicePluginBose() override; + + void init() override; + Device::DeviceSetupStatus setupDevice(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: + PluginTimer *m_pluginTimer = nullptr; + + QHash m_soundTouch; + QHash m_pendingActions; + +private slots: + void onPluginTimer(); + void onConnectionChanged(bool connected); + void onDeviceNameChanged(); + + void onInfoObjectReceived(InfoObject infoObject); + void onNowPlayingObjectReceived(NowPlayingObject nowPlaying); + void onVolumeObjectReceived(VolumeObject volume); + void onSourcesObjectReceived(SourcesObject sources); +}; + +#endif // DEVICEPLUGINBOSE_H diff --git a/bose/devicepluginbose.json b/bose/devicepluginbose.json new file mode 100644 index 00000000..9d6a0575 --- /dev/null +++ b/bose/devicepluginbose.json @@ -0,0 +1,187 @@ +{ + "id": "472a3f24-b05c-49b3-ad9a-dfda608b6760", + "name": "Bose", + "displayName": "Bose", + "vendors": [ + { + "id": "433c45cd-5bc1-4239-a8a1-487c70ffdfc7", + "name": "bose", + "displayName": "Bose", + "deviceClasses": [ + { + "id": "f9b7a3f5-6353-48b1-afc1-66f914412f82", + "name": "soundtouch", + "displayName": "SoundTouch", + "interfaces": ["extendedvolumecontroller", "mediametadataprovider", "shufflerepeat", "connectable"], + "createMethods": ["discovery"], + "paramTypes": [ + { + "id": "1a897065-57c6-49b3-bac9-1e5db27859e5", + "name": "ip", + "displayName": "IP", + "type" : "QString", + "inputType": "IPv4Address" + }, + { + "id": "3eb95eef-e8ba-4d44-8a21-7d8038b74c4d", + "name": "playerId", + "displayName": "Player ID", + "type" : "QString" + } + ], + "stateTypes": [ + { + "id": "09dfbd40-c97c-4a20-9ecd-f80e389a4864", + "name": "connected", + "displayName": "Connected", + "displayNameEvent": "connected changed", + "defaultValue": false, + "type": "bool" + }, + { + "id": "5bac4ad7-f55c-4301-8d72-f2783d9909ff", + "name": "power", + "displayName": "Power", + "displayNameEvent": "Power changed", + "displayNameAction": "Set power", + "defaultValue": false, + "type": "bool", + "writable": true + }, + { + "id": "bc98cdb0-4d0e-48ca-afc7-922e49bb7813", + "name": "mute", + "displayName": "mute", + "displayNameEvent": "Mute changed", + "displayNameAction": "Set mute", + "type": "bool", + "defaultValue": false, + "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": "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" + } + ] + } + ] + } + ] +} From 9d30ec249f7685b56cc3cdfc7c22c8cb724f5eb6 Mon Sep 17 00:00:00 2001 From: nymea Date: Thu, 11 Jul 2019 17:44:29 +0200 Subject: [PATCH 5/9] fixed power state and added bass setting --- bose/devicepluginbose.cpp | 36 ++++++++++++++++++- bose/devicepluginbose.h | 2 ++ bose/devicepluginbose.json | 44 ++++++++++++++++------- bose/soundtouch.cpp | 73 ++++++++++++++++++++++++++++++++++++-- 4 files changed, 140 insertions(+), 15 deletions(-) diff --git a/bose/devicepluginbose.cpp b/bose/devicepluginbose.cpp index e484e31c..25d66679 100644 --- a/bose/devicepluginbose.cpp +++ b/bose/devicepluginbose.cpp @@ -61,11 +61,17 @@ Device::DeviceSetupStatus DevicePluginBose::setupDevice(Device *device) connect(soundTouch, &SoundTouch::nowPlayingReceived, this, &DevicePluginBose::onNowPlayingObjectReceived); connect(soundTouch, &SoundTouch::volumeReceived, this, &DevicePluginBose::onVolumeObjectReceived); connect(soundTouch, &SoundTouch::sourcesReceived, this, &DevicePluginBose::onSourcesObjectReceived); + connect(soundTouch, &SoundTouch::bassReceived, this, &DevicePluginBose::onBassObjectReceived); + connect(soundTouch, &SoundTouch::bassCapabilitiesReceived, this, &DevicePluginBose::onBassCapabilitiesObjectReceived); soundTouch->getInfo(); soundTouch->getNowPlaying(); soundTouch->getVolume(); soundTouch->getSources(); + soundTouch->getBass(); + soundTouch->getBassCapabilities(); + soundTouch->getGroup(); + soundTouch->getZone(); m_soundTouch.insert(device, soundTouch); @@ -77,7 +83,7 @@ Device::DeviceSetupStatus DevicePluginBose::setupDevice(Device *device) void DevicePluginBose::deviceRemoved(Device *device) { if (device->deviceClassId() == soundtouchDeviceClassId) { - SoundTouch *soundTouch = m_soundTouch.value(device); + SoundTouch *soundTouch = m_soundTouch.take(device); soundTouch->deleteLater(); } @@ -100,6 +106,13 @@ Device::DeviceError DevicePluginBose::discoverDevices(const DeviceClassId &devic QString playerId = avahiEntry.hostName().split(".").first(); DeviceDescriptor descriptor(soundtouchDeviceClassId, avahiEntry.name(), avahiEntry.hostAddress().toString()); ParamList params; + + foreach (Device *existingDevice, myDevices().filterByDeviceClassId(soundtouchDeviceClassId)) { + if (existingDevice->paramValue(soundtouchDevicePlayerIdParamTypeId).toString() == playerId) { + descriptor.setDeviceId(existingDevice->id()); + break; + } + } params << Param(soundtouchDeviceIpParamTypeId, avahiEntry.hostAddress().toString()); params << Param(soundtouchDevicePlayerIdParamTypeId, playerId); descriptor.setParams(params); @@ -177,6 +190,12 @@ Device::DeviceError DevicePluginBose::executeAction(Device *device, const Action return Device::DeviceErrorNoError; } + if (action.actionTypeId() == soundtouchBassActionTypeId) { + int bass = action.param(soundtouchBassActionBassParamTypeId).value().toInt(); + soundTouch->setBass(bass); + return Device::DeviceErrorNoError; + } + if (action.actionTypeId() == soundtouchPlaybackStatusActionTypeId) { QString status = action.param(soundtouchPlaybackStatusActionPlaybackStatusParamTypeId).value().toString(); if (status == "Playing") { @@ -200,6 +219,7 @@ void DevicePluginBose::onPluginTimer() soundTouch->getInfo(); soundTouch->getNowPlaying(); soundTouch->getVolume(); + soundTouch->getBass(); } } @@ -229,6 +249,8 @@ void DevicePluginBose::onNowPlayingObjectReceived(NowPlayingObject nowPlaying) SoundTouch *soundtouch = static_cast(sender()); Device *device = m_soundTouch.key(soundtouch); + device->setStateValue(soundtouchPowerStateTypeId, !(nowPlaying.source.toUpper() == "STANDBY")); + device->setStateValue(soundtouchSourceStateTypeId, nowPlaying.source); device->setStateValue(soundtouchTitleStateTypeId, nowPlaying.track); device->setStateValue(soundtouchArtistStateTypeId, nowPlaying.artist); device->setStateValue(soundtouchCollectionStateTypeId, nowPlaying.album); @@ -275,3 +297,15 @@ void DevicePluginBose::onSourcesObjectReceived(SourcesObject sources) qDebug(dcBose()) << "Source:" << sources.deviceId << sourceItem.source << sourceItem.displayName; } } + +void DevicePluginBose::onBassObjectReceived(BassObject bass) +{ + SoundTouch *soundtouch = static_cast(sender()); + Device *device = m_soundTouch.key(soundtouch); + device->setStateValue(soundtouchBassStateTypeId, bass.actualBass); +} + +void DevicePluginBose::onBassCapabilitiesObjectReceived(BassCapabilitiesObject bassCapabilities) +{ + qDebug(dcBose()) << "Bass capabilities (max, min, default):" << bassCapabilities.bassMax << bassCapabilities.bassMin << bassCapabilities.bassDefault; +} diff --git a/bose/devicepluginbose.h b/bose/devicepluginbose.h index 382033a6..72a3711e 100644 --- a/bose/devicepluginbose.h +++ b/bose/devicepluginbose.h @@ -62,6 +62,8 @@ private slots: void onNowPlayingObjectReceived(NowPlayingObject nowPlaying); void onVolumeObjectReceived(VolumeObject volume); void onSourcesObjectReceived(SourcesObject sources); + void onBassObjectReceived(BassObject bass); + void onBassCapabilitiesObjectReceived(BassCapabilitiesObject bassCapabilities); }; #endif // DEVICEPLUGINBOSE_H diff --git a/bose/devicepluginbose.json b/bose/devicepluginbose.json index 9d6a0575..f1c911c7 100644 --- a/bose/devicepluginbose.json +++ b/bose/devicepluginbose.json @@ -51,7 +51,7 @@ { "id": "bc98cdb0-4d0e-48ca-afc7-922e49bb7813", "name": "mute", - "displayName": "mute", + "displayName": "Mute", "displayNameEvent": "Mute changed", "displayNameAction": "Set mute", "type": "bool", @@ -61,8 +61,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", @@ -74,7 +74,7 @@ { "id": "2dd512b7-40c2-488e-8d4f-6519edaa6f74", "name": "playbackStatus", - "displayName": "playback status", + "displayName": "Playback status", "type": "QString", "possibleValues": ["Playing", "Paused", "Stopped"], "defaultValue": "Stopped", @@ -134,50 +134,70 @@ "possibleValues": ["None", "One", "All"], "defaultValue": "None", "writable": true + }, + { + "id": "f4684de8-ff5f-41f3-a5c3-f5e5754519d2", + "type": "QString", + "name": "source", + "displayName": "Source", + "displayNameEvent": "Source changed", + "defaultValue": "None" + }, + { + "id": "91bc53ec-4f3b-438d-8e32-129b7c27aae4", + "name": "bass", + "displayName": "Bass", + "displayNameEvent": "Bass changed", + "displayNameAction": "Set bass", + "type": "int", + "minValue": -9, + "maxValue": 0, + "defaultValue": 0, + "writable": true } ], "eventTypes": [ { "id": "2535a1eb-7643-4874-98f6-b027fdff6311", "name": "onPlayerPlay", - "displayName": "player play" + "displayName": "Player play" }, { "id": "99498b1c-e9c0-480a-9e91-662ee79ba976", "name": "onPlayerPause", - "displayName": "player pause" + "displayName": "Player pause" }, { "id": "a02ce255-3abb-435d-a92e-7f99c952ecb2", "name": "onPlayerStop", - "displayName": "player stop" + "displayName": "Player stop" } ], "actionTypes": [ { "id": "a180807d-1265-4831-9d86-a421767418dd", "name": "skipBack", - "displayName": "skip back" + "displayName": "Skip back" }, { "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" } ] } diff --git a/bose/soundtouch.cpp b/bose/soundtouch.cpp index 6738ccf2..28dcd92d 100644 --- a/bose/soundtouch.cpp +++ b/bose/soundtouch.cpp @@ -123,7 +123,7 @@ void SoundTouch::getZone() url.setScheme("http"); url.setPort(m_port); url.setPath("/getZone"); - qDebug(dcBose) << "Sending request" << url; + //qDebug(dcBose) << "Sending request" << url; QNetworkReply *reply = m_networkAccessManager->get(QNetworkRequest(url)); connect(reply, &QNetworkReply::finished, this, &SoundTouch::onRestRequestFinished); } @@ -140,6 +140,18 @@ void SoundTouch::getPresets() connect(reply, &QNetworkReply::finished, this, &SoundTouch::onRestRequestFinished); } +void SoundTouch::getBassCapabilities() +{ + QUrl url; + url.setHost(m_ipAddress); + url.setScheme("http"); + url.setPort(m_port); + url.setPath("/bassCapabilities"); + qDebug(dcBose) << "Sending request" << url; + QNetworkReply *reply = m_networkAccessManager->get(QNetworkRequest(url)); + connect(reply, &QNetworkReply::finished, this, &SoundTouch::onRestRequestFinished); +} + void SoundTouch::setKey(KEY_VALUE keyValue) { QUrl url; @@ -398,7 +410,6 @@ void SoundTouch::onWebsocketDisconnected() }); } - void SoundTouch::onRestRequestFinished() { QNetworkReply *reply = static_cast(sender()); @@ -627,6 +638,64 @@ void SoundTouch::onRestRequestFinished() { } } emit sourcesReceived(sourcesObject); + } else if (xml.name() == "bass") { + BassObject bassObject; + if(xml.attributes().hasAttribute("deviceID")) { + //qDebug(dcBose) << "Device ID" << xml.attributes().value("deviceID").toString(); + bassObject.deviceID = xml.attributes().value("deviceID").toString(); + } + while(xml.readNextStartElement()){ + if(xml.name() == "targetbass"){ + //qDebug(dcBose) << "Target bas" << xml.readElementText(); + bassObject.targetBass = xml.readElementText().toInt(); + } else if(xml.name() == "actualbass"){ + //qDebug(dcBose) << "Actual bass" << xml.readElementText(); + bassObject.actualBass = xml.readElementText().toInt(); + }else { + xml.skipCurrentElement(); + } + } + emit bassReceived(bassObject); + } else if (xml.name() == "bassCapabilities") { + BassCapabilitiesObject bassCapabilities; + + emit bassCapabilitiesReceived(bassCapabilities); + } else if (xml.name() == "presets") { + PresetObject preset; + if(xml.attributes().hasAttribute("id")) { + preset.presetId = xml.attributes().value("id").toInt(); + } + if(xml.attributes().hasAttribute("createdOn")) { + preset.createdOn= xml.attributes().value("createdOn").toULong(); + } + if(xml.attributes().hasAttribute("updatedOn")) { + preset.updatedOn = xml.attributes().value("updatedOn").toULong(); + } + while(xml.readNextStartElement()){ + if(xml.name() == "ContentItem"){ + if(xml.attributes().hasAttribute("source")) { + preset.ContentItem.source = xml.attributes().value("source").toString(); + } + if(xml.attributes().hasAttribute("location")) { + preset.ContentItem.location = xml.attributes().value("location").toString(); + } + if(xml.attributes().hasAttribute("sourceAccount")) { + preset.ContentItem.sourceAccount = xml.attributes().value("sourceAccount").toString(); + } + }else { + xml.skipCurrentElement(); + } + } + emit presetsReceived(preset); + + } else if (xml.name() == "group") { + GroupObject group; + if(xml.attributes().hasAttribute("deviceID")) { + //qDebug(dcBose) << "Device ID" << xml.attributes().value("deviceID").toString(); + group.id = xml.attributes().value("id").toString(); + } + + emit groupReceived(group); } else { xml.skipCurrentElement(); } From aa25f987627efd98550da753d2fc6d3096daa99d Mon Sep 17 00:00:00 2001 From: nymea Date: Thu, 11 Jul 2019 21:17:47 +0200 Subject: [PATCH 6/9] added zones and groups --- bose/devicepluginbose.cpp | 22 ++++++++- bose/devicepluginbose.h | 2 + bose/soundtouch.cpp | 100 ++++++++++++++++++++++++++++++++------ bose/soundtouch.h | 1 + bose/soundtouchtypes.h | 6 +-- 5 files changed, 113 insertions(+), 18 deletions(-) diff --git a/bose/devicepluginbose.cpp b/bose/devicepluginbose.cpp index 25d66679..ea0ce81b 100644 --- a/bose/devicepluginbose.cpp +++ b/bose/devicepluginbose.cpp @@ -63,6 +63,8 @@ Device::DeviceSetupStatus DevicePluginBose::setupDevice(Device *device) connect(soundTouch, &SoundTouch::sourcesReceived, this, &DevicePluginBose::onSourcesObjectReceived); connect(soundTouch, &SoundTouch::bassReceived, this, &DevicePluginBose::onBassObjectReceived); connect(soundTouch, &SoundTouch::bassCapabilitiesReceived, this, &DevicePluginBose::onBassCapabilitiesObjectReceived); + connect(soundTouch, &SoundTouch::groupReceived, this, &DevicePluginBose::onGroupObjectReceived); + connect(soundTouch, &SoundTouch::zoneReceived, this, &DevicePluginBose::onZoneObjectReceived); soundTouch->getInfo(); soundTouch->getNowPlaying(); @@ -220,6 +222,8 @@ void DevicePluginBose::onPluginTimer() soundTouch->getNowPlaying(); soundTouch->getVolume(); soundTouch->getBass(); + soundTouch->getGroup(); + soundTouch->getZone(); } } @@ -307,5 +311,21 @@ void DevicePluginBose::onBassObjectReceived(BassObject bass) void DevicePluginBose::onBassCapabilitiesObjectReceived(BassCapabilitiesObject bassCapabilities) { - qDebug(dcBose()) << "Bass capabilities (max, min, default):" << bassCapabilities.bassMax << bassCapabilities.bassMin << bassCapabilities.bassDefault; + qDebug(dcBose()) << "Bass capabilities (max, min, default):" << bassCapabilities.bassMax << bassCapabilities.bassMin << bassCapabilities.bassDefault; +} + +void DevicePluginBose::onGroupObjectReceived(GroupObject group) +{ + qDebug(dcBose()) << "Group" << group.name << group.status; + foreach (RolesObject role, group.roles) { + qDebug(dcBose()) << "-> member:" << role.groupRole.deviceID; + } +} + +void DevicePluginBose::onZoneObjectReceived(ZoneObject zone) +{ + qDebug(dcBose()) << "Zone master" << zone.deviceID; + foreach (MemberObject member, zone.members) { + qDebug(dcBose()) << "-> member:" << member.deviceID; + } } diff --git a/bose/devicepluginbose.h b/bose/devicepluginbose.h index 72a3711e..48e953b0 100644 --- a/bose/devicepluginbose.h +++ b/bose/devicepluginbose.h @@ -64,6 +64,8 @@ private slots: void onSourcesObjectReceived(SourcesObject sources); void onBassObjectReceived(BassObject bass); void onBassCapabilitiesObjectReceived(BassCapabilitiesObject bassCapabilities); + void onGroupObjectReceived(GroupObject group); + void onZoneObjectReceived(ZoneObject zone); }; #endif // DEVICEPLUGINBOSE_H diff --git a/bose/soundtouch.cpp b/bose/soundtouch.cpp index 28dcd92d..af440bdb 100644 --- a/bose/soundtouch.cpp +++ b/bose/soundtouch.cpp @@ -22,6 +22,7 @@ #include "soundtouch.h" #include "hardwaremanager.h" +#include "soundtouch.h" #include "devices/device.h" #include "network/networkaccessmanager.h" @@ -54,6 +55,7 @@ void SoundTouch::getInfo() //qDebug(dcBose) << "Sending request" << url; QNetworkReply *reply = m_networkAccessManager->get(QNetworkRequest(url)); connect(reply, &QNetworkReply::finished, this, &SoundTouch::onRestRequestFinished); + connect(reply, SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(onRestRequestError(QNetworkReply::NetworkError))); } void SoundTouch::getVolume() @@ -98,8 +100,8 @@ void SoundTouch::getGroup() url.setHost(m_ipAddress); url.setScheme("http"); url.setPort(m_port); - url.setPath("/group"); - qDebug(dcBose) << "Sending request" << url; + url.setPath("/getGroup"); + //qDebug(dcBose) << "Sending request" << url; QNetworkReply *reply = m_networkAccessManager->get(QNetworkRequest(url)); connect(reply, &QNetworkReply::finished, this, &SoundTouch::onRestRequestFinished); } @@ -135,7 +137,7 @@ void SoundTouch::getPresets() url.setScheme("http"); url.setPort(m_port); url.setPath("/presets"); - qDebug(dcBose) << "Sending request" << url; + //qDebug(dcBose) << "Sending request" << url; QNetworkReply *reply = m_networkAccessManager->get(QNetworkRequest(url)); connect(reply, &QNetworkReply::finished, this, &SoundTouch::onRestRequestFinished); } @@ -147,7 +149,7 @@ void SoundTouch::getBassCapabilities() url.setScheme("http"); url.setPort(m_port); url.setPath("/bassCapabilities"); - qDebug(dcBose) << "Sending request" << url; + //qDebug(dcBose) << "Sending request" << url; QNetworkReply *reply = m_networkAccessManager->get(QNetworkRequest(url)); connect(reply, &QNetworkReply::finished, this, &SoundTouch::onRestRequestFinished); } @@ -284,8 +286,10 @@ void SoundTouch::setZone(ZoneObject zone) xml.writeStartDocument(); xml.writeStartElement("zone"); xml.writeAttribute("master", zone.deviceID); - xml.writeTextElement("member", zone.member.deviceID); - xml.writeAttribute("ipaddress", zone.member.ipAddress); + foreach (MemberObject member, zone.members){ + xml.writeTextElement("member", member.deviceID); + xml.writeAttribute("ipaddress", member.ipAddress); + } xml.writeEndElement(); //zone xml.writeEndDocument(); qDebug(dcBose) << "Sending request" << url << content; @@ -305,8 +309,10 @@ void SoundTouch::addZoneSlave(ZoneObject zone) xml.writeStartDocument(); xml.writeStartElement("zone"); xml.writeAttribute("master", zone.deviceID); - xml.writeTextElement("member", zone.member.deviceID); - xml.writeAttribute("ipaddress", zone.member.ipAddress); + foreach (MemberObject member, zone.members){ + xml.writeTextElement("member", member.deviceID); + xml.writeAttribute("ipaddress", member.ipAddress); + } xml.writeEndElement(); //zone xml.writeEndDocument(); qDebug(dcBose) << "Sending request" << url << content; @@ -326,8 +332,10 @@ void SoundTouch::removeZoneSlave(ZoneObject zone) xml.writeStartDocument(); xml.writeStartElement("zone"); xml.writeAttribute("master", zone.deviceID); - xml.writeTextElement("member", zone.member.deviceID); - xml.writeAttribute("ipaddress", zone.member.ipAddress); + foreach (MemberObject member, zone.members){ + xml.writeTextElement("member", member.deviceID); + xml.writeAttribute("ipaddress", member.ipAddress); + } xml.writeEndElement(); //zone xml.writeEndDocument(); qDebug(dcBose) << "Sending request" << url << content; @@ -651,7 +659,7 @@ void SoundTouch::onRestRequestFinished() { } else if(xml.name() == "actualbass"){ //qDebug(dcBose) << "Actual bass" << xml.readElementText(); bassObject.actualBass = xml.readElementText().toInt(); - }else { + } else { xml.skipCurrentElement(); } } @@ -659,6 +667,27 @@ void SoundTouch::onRestRequestFinished() { } else if (xml.name() == "bassCapabilities") { BassCapabilitiesObject bassCapabilities; + if(xml.attributes().hasAttribute("deviceID")) { + bassCapabilities.deviceID = xml.attributes().value("deviceID").toString(); + } + + while(xml.readNextStartElement()){ + if(xml.name() == "bassAvailable"){ + //qDebug(dcBose) << "BassAvailable" << xml.readElementText(); + bassCapabilities.bassAvailable = ( xml.readElementText().toUpper() == "TRUE" ); + } else if(xml.name() == "bassMin"){ + //qDebug(dcBose) << "bass Min" << xml.readElementText(); + bassCapabilities.bassMin = xml.readElementText().toInt(); + } else if(xml.name() == "bassMax"){ + //qDebug(dcBose) << "bass Max" << xml.readElementText(); + bassCapabilities.bassMax = xml.readElementText().toInt(); + } else if(xml.name() == "bassDefault"){ + //qDebug(dcBose) << "bass default" << xml.readElementText(); + bassCapabilities.bassDefault = xml.readElementText().toInt(); + }else { + xml.skipCurrentElement(); + } + } emit bassCapabilitiesReceived(bassCapabilities); } else if (xml.name() == "presets") { PresetObject preset; @@ -691,19 +720,62 @@ void SoundTouch::onRestRequestFinished() { } else if (xml.name() == "group") { GroupObject group; if(xml.attributes().hasAttribute("deviceID")) { - //qDebug(dcBose) << "Device ID" << xml.attributes().value("deviceID").toString(); group.id = xml.attributes().value("id").toString(); } - + while(xml.readNextStartElement()){ + if(xml.name() == "name") { + group.name = xml.readElementText(); + } else if(xml.name() == "masterDeviceId") { + group.masterDeviceId = xml.readElementText(); + } else if(xml.name() == "roles") { + //group.roles = xml.readElementText().toInt(); + } else if(xml.name() == "status"){ + QString groupStatus = xml.readElementText(); + //qDebug(dcBose) << "Group role" << groupStatus; + //group.status = xml.readElementText(); + }else { + xml.skipCurrentElement(); + } + } emit groupReceived(group); - } else { + } else if (xml.name() == "zone") { + ZoneObject zone; + if(xml.attributes().hasAttribute("master")) { + zone.deviceID = xml.attributes().value("master").toString(); + } + while(xml.readNextStartElement()){ + MemberObject member; + if(xml.name() == "member") { + if(xml.attributes().hasAttribute("ipaddress")) { + member.ipAddress = xml.attributes().value("ipaddress").toString(); + } + member.deviceID = xml.readElementText(); + } else { + xml.skipCurrentElement(); + } + zone.members.append(member); + } + emit zoneReceived(zone); + } + else { xml.skipCurrentElement(); } } } +void SoundTouch::onRestRequestError(QNetworkReply::NetworkError error) +{ + Q_UNUSED(error) + + QNetworkReply *reply = static_cast(sender()); + reply->deleteLater(); + + qWarning(dcBose) << "Rest Error" << reply->errorString(); +} + void SoundTouch::onWebsocketMessageReceived(QString message) { qDebug(dcBose) << "Websocket message received:" << message; + } diff --git a/bose/soundtouch.h b/bose/soundtouch.h index 69111cbc..887e5fc7 100644 --- a/bose/soundtouch.h +++ b/bose/soundtouch.h @@ -86,6 +86,7 @@ private slots: void onWebsocketDisconnected(); void onWebsocketMessageReceived(QString message); void onRestRequestFinished(); + void onRestRequestError(QNetworkReply::NetworkError error); }; #endif // SOUNDTOUCH_H diff --git a/bose/soundtouchtypes.h b/bose/soundtouchtypes.h index 4ecac7e1..930d1ce1 100644 --- a/bose/soundtouchtypes.h +++ b/bose/soundtouchtypes.h @@ -195,8 +195,8 @@ struct MemberObject { }; struct ZoneObject { - QString deviceID; //Attribute. The deviceID unique identifier of the master product. - MemberObject member; //Element. This object describes products in the zone There is an object for each product. + QString deviceID; //Attribute. The deviceID unique identifier of the master product. + QList members; //Element. This object describes products in the zone There is an object for each product. }; struct BassCapabilitiesObject { @@ -234,7 +234,7 @@ struct GroupObject { QString id; //Attribute. A unique ID for the group. QString name; //Element. A user-set name for the group. QString masterDeviceId; //Element. The unique identifier of the master product in the group. - RolesObject roles; //Element. This object describes the products in the group and their location (left/right). + QList roles; //Element. This object describes the products in the group and their location (left/right). PLAY_STATUS status; //Element. The state of the stereo pair group. }; From 5450a96a001340a4f1612d8412ef55a2962f223d Mon Sep 17 00:00:00 2001 From: nymea Date: Fri, 12 Jul 2019 14:18:55 +0200 Subject: [PATCH 7/9] fixed polling with queue --- bose/devicepluginbose.cpp | 3 - bose/soundtouch.cpp | 242 ++++++++++++++++++++------------------ bose/soundtouch.h | 12 ++ 3 files changed, 140 insertions(+), 117 deletions(-) diff --git a/bose/devicepluginbose.cpp b/bose/devicepluginbose.cpp index ea0ce81b..36862c76 100644 --- a/bose/devicepluginbose.cpp +++ b/bose/devicepluginbose.cpp @@ -63,7 +63,6 @@ Device::DeviceSetupStatus DevicePluginBose::setupDevice(Device *device) connect(soundTouch, &SoundTouch::sourcesReceived, this, &DevicePluginBose::onSourcesObjectReceived); connect(soundTouch, &SoundTouch::bassReceived, this, &DevicePluginBose::onBassObjectReceived); connect(soundTouch, &SoundTouch::bassCapabilitiesReceived, this, &DevicePluginBose::onBassCapabilitiesObjectReceived); - connect(soundTouch, &SoundTouch::groupReceived, this, &DevicePluginBose::onGroupObjectReceived); connect(soundTouch, &SoundTouch::zoneReceived, this, &DevicePluginBose::onZoneObjectReceived); soundTouch->getInfo(); @@ -72,7 +71,6 @@ Device::DeviceSetupStatus DevicePluginBose::setupDevice(Device *device) soundTouch->getSources(); soundTouch->getBass(); soundTouch->getBassCapabilities(); - soundTouch->getGroup(); soundTouch->getZone(); m_soundTouch.insert(device, soundTouch); @@ -222,7 +220,6 @@ void DevicePluginBose::onPluginTimer() soundTouch->getNowPlaying(); soundTouch->getVolume(); soundTouch->getBass(); - soundTouch->getGroup(); soundTouch->getZone(); } } diff --git a/bose/soundtouch.cpp b/bose/soundtouch.cpp index af440bdb..24ed1f54 100644 --- a/bose/soundtouch.cpp +++ b/bose/soundtouch.cpp @@ -22,7 +22,6 @@ #include "soundtouch.h" #include "hardwaremanager.h" -#include "soundtouch.h" #include "devices/device.h" #include "network/networkaccessmanager.h" @@ -47,111 +46,93 @@ SoundTouch::SoundTouch(NetworkAccessManager *networkAccessManager, QString ipAdd void SoundTouch::getInfo() { - QUrl url; - url.setHost(m_ipAddress); - url.setScheme("http"); - url.setPort(m_port); - url.setPath("/info"); - //qDebug(dcBose) << "Sending request" << url; - QNetworkReply *reply = m_networkAccessManager->get(QNetworkRequest(url)); - connect(reply, &QNetworkReply::finished, this, &SoundTouch::onRestRequestFinished); - connect(reply, SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(onRestRequestError(QNetworkReply::NetworkError))); + if (!m_getRepliesPending) { + sendGetRequest("/info"); + } else { + if (!m_getRequestQueue.contains("/info")) + m_getRequestQueue.append("/info"); + } } void SoundTouch::getVolume() { - QUrl url; - url.setHost(m_ipAddress); - url.setScheme("http"); - url.setPort(m_port); - url.setPath("/volume"); - //qDebug(dcBose) << "Sending request" << url; - QNetworkReply *reply = m_networkAccessManager->get(QNetworkRequest(url)); - connect(reply, &QNetworkReply::finished, this, &SoundTouch::onRestRequestFinished); + if (!m_getRepliesPending) { + sendGetRequest("/volume"); + } else { + if (!m_getRequestQueue.contains("/volume")) + m_getRequestQueue.append("/volume"); + } } void SoundTouch::getNowPlaying() { - QUrl url; - url.setHost(m_ipAddress); - url.setScheme("http"); - url.setPort(m_port); - url.setPath("/now_playing"); - //qDebug(dcBose) << "Sending request" << url; - QNetworkReply *reply = m_networkAccessManager->get(QNetworkRequest(url)); - connect(reply, &QNetworkReply::finished, this, &SoundTouch::onRestRequestFinished); + if (!m_getRepliesPending) { + sendGetRequest("/now_playing"); + } else { + if (!m_getRequestQueue.contains("/now_playing")) + m_getRequestQueue.append("/now_playing"); + } } void SoundTouch::getBass() { - QUrl url; - url.setHost(m_ipAddress); - url.setScheme("http"); - url.setPort(m_port); - url.setPath("/bass"); - //qDebug(dcBose) << "Sending request" << url; - QNetworkReply *reply = m_networkAccessManager->get(QNetworkRequest(url)); - connect(reply, &QNetworkReply::finished, this, &SoundTouch::onRestRequestFinished); + if (!m_getRepliesPending) { + sendGetRequest("/bass"); + } else { + if (!m_getRequestQueue.contains("/bass")) + m_getRequestQueue.append("/bass"); + } } void SoundTouch::getGroup() { - QUrl url; - url.setHost(m_ipAddress); - url.setScheme("http"); - url.setPort(m_port); - url.setPath("/getGroup"); - //qDebug(dcBose) << "Sending request" << url; - QNetworkReply *reply = m_networkAccessManager->get(QNetworkRequest(url)); - connect(reply, &QNetworkReply::finished, this, &SoundTouch::onRestRequestFinished); + if (!m_getRepliesPending) { + sendGetRequest("/getGroup"); + } else { + if (!m_getRequestQueue.contains("/getGroup")) + m_getRequestQueue.append("/getGroup"); + } } void SoundTouch::getSources() { - QUrl url; - url.setHost(m_ipAddress); - url.setScheme("http"); - url.setPort(m_port); - url.setPath("/sources"); - //qDebug(dcBose) << "Sending request" << url; - QNetworkReply *reply = m_networkAccessManager->get(QNetworkRequest(url)); - connect(reply, &QNetworkReply::finished, this, &SoundTouch::onRestRequestFinished); + if (!m_getRepliesPending) { + sendGetRequest("/sources"); + } else { + if (!m_getRequestQueue.contains("/sources")) + m_getRequestQueue.append("/sources"); + } } void SoundTouch::getZone() { - QUrl url; - url.setHost(m_ipAddress); - url.setScheme("http"); - url.setPort(m_port); - url.setPath("/getZone"); - //qDebug(dcBose) << "Sending request" << url; - QNetworkReply *reply = m_networkAccessManager->get(QNetworkRequest(url)); - connect(reply, &QNetworkReply::finished, this, &SoundTouch::onRestRequestFinished); + if (!m_getRepliesPending) { + sendGetRequest("/getZone"); + } else { + if (!m_getRequestQueue.contains("/getZone")) + m_getRequestQueue.append("/getZone"); + } } void SoundTouch::getPresets() { - QUrl url; - url.setHost(m_ipAddress); - url.setScheme("http"); - url.setPort(m_port); - url.setPath("/presets"); - //qDebug(dcBose) << "Sending request" << url; - QNetworkReply *reply = m_networkAccessManager->get(QNetworkRequest(url)); - connect(reply, &QNetworkReply::finished, this, &SoundTouch::onRestRequestFinished); + if (!m_getRepliesPending) { + sendGetRequest("/presets"); + } else { + if (!m_getRequestQueue.contains("/presets")) + m_getRequestQueue.append("/presets"); + } } + void SoundTouch::getBassCapabilities() { - QUrl url; - url.setHost(m_ipAddress); - url.setScheme("http"); - url.setPort(m_port); - url.setPath("/bassCapabilities"); - //qDebug(dcBose) << "Sending request" << url; - QNetworkReply *reply = m_networkAccessManager->get(QNetworkRequest(url)); - connect(reply, &QNetworkReply::finished, this, &SoundTouch::onRestRequestFinished); + if (!m_getRepliesPending) { + sendGetRequest("/bassCapabilities"); + } else { + if (!m_getRequestQueue.contains("/bassCapabilities")) + m_getRequestQueue.append("/bassCapabilities"); + } } void SoundTouch::setKey(KEY_VALUE keyValue) @@ -161,62 +142,67 @@ void SoundTouch::setKey(KEY_VALUE keyValue) url.setScheme("http"); url.setPort(m_port); url.setPath("/key"); - QByteArray content = (""); - content.append(""); + QByteArray content; + QXmlStreamWriter xml(&content); + xml.writeStartDocument("1.0"); + xml.writeStartElement("key"); + xml.writeAttribute("state", "press"); + xml.writeAttribute("sender", "Gabbo"); switch (keyValue){ case KEY_VALUE_PLAY: - content.append("PLAY"); + xml.writeCharacters("PLAY"); break; case KEY_VALUE_STOP: - content.append("STOP"); + xml.writeCharacters("STOP"); break; case KEY_VALUE_PAUSE: - content.append("PAUSE"); + xml.writeCharacters("PAUSE"); break; case KEY_VALUE_PLAY_PAUSE: - content.append("PLAY_PAUSE"); + xml.writeCharacters("PLAY_PAUSE"); break; case KEY_VALUE_POWER: - content.append("POWER"); + xml.writeCharacters("POWER"); break; case KEY_VALUE_NEXT_TRACK: - content.append("NEXT_TRACK"); + xml.writeCharacters("NEXT_TRACK"); break; case KEY_VALUE_PREV_TRACK: - content.append("PREV_TRACK"); + xml.writeCharacters("PREV_TRACK"); break; case KEY_VALUE_BOOKMARK: - content.append("BOOKMARK"); + xml.writeCharacters("BOOKMARK"); break; case KEY_VALUE_AUX_INPUT: - content.append("AUX_INPUT"); + xml.writeCharacters("AUX_INPUT"); break; case KEY_VALUE_REPEAT_ALL: - content.append("REPEAT_ALL"); + xml.writeCharacters("REPEAT_ALL"); break; case KEY_VALUE_REPEAT_ONE: - content.append("REPEAT_ONE"); + xml.writeCharacters("REPEAT_ONE"); break; case KEY_VALUE_REPEAT_OFF: - content.append("REPEAT_OFF"); + xml.writeCharacters("REPEAT_OFF"); break; case KEY_VALUE_ADD_FAVORITE: - content.append("ADD_FAVORITE"); + xml.writeCharacters("ADD_FAVORITE"); break; case KEY_VALUE_MUTE: - content.append("MUTE"); + xml.writeCharacters("MUTE"); break; case KEY_VALUE_SHUFFLE_ON: - content.append("SHUFFLE_ON"); + xml.writeCharacters("SHUFFLE_ON"); break; case KEY_VALUE_SHUFFLE_OFF: - content.append("SHUFFLE_OFF"); + xml.writeCharacters("SHUFFLE_OFF"); break; default: qWarning(dcBose) << "key not yet implemented"; return; } - content.append(""); + xml.writeEndElement(); //key + xml.writeEndDocument(); qDebug(dcBose) << "Sending request" << url << content; QNetworkReply *reply = m_networkAccessManager->post(QNetworkRequest(url), content); connect(reply, &QNetworkReply::finished, this, &SoundTouch::onRestRequestFinished); @@ -227,11 +213,15 @@ void SoundTouch::setKey(KEY_VALUE keyValue) url.setScheme("http"); url.setPort(m_port); url.setPath("/key"); - QByteArray content = (""); - content.append(""); - content.append("POWER"); - content.append(""); - //qDebug(dcBose) << "Sending request" << url << content; + QByteArray content; + QXmlStreamWriter xml(&content); + xml.writeStartDocument("1.0"); + xml.writeStartElement("key"); + xml.writeAttribute("state", "release"); + xml.writeAttribute("sender", "Gabbo"); + xml.writeCharacters("POWER"); + xml.writeEndElement(); //key + xml.writeEndDocument(); QNetworkReply *reply = m_networkAccessManager->post(QNetworkRequest(url), content); connect(reply, &QNetworkReply::finished, this, &SoundTouch::onRestRequestFinished); } @@ -270,8 +260,8 @@ void SoundTouch::setSource(ContentItemObject contentItem) xml.writeEndDocument(); qDebug(dcBose) << "Sending request" << url << content; - //QNetworkReply *reply = m_networkAccessManager->post(QNetworkRequest(url), content); - //connect(reply, &QNetworkReply::finished, this, &SoundTouch::onRestRequestFinished); + QNetworkReply *reply = m_networkAccessManager->post(QNetworkRequest(url), content); + connect(reply, &QNetworkReply::finished, this, &SoundTouch::onRestRequestFinished); } void SoundTouch::setZone(ZoneObject zone) @@ -283,7 +273,7 @@ void SoundTouch::setZone(ZoneObject zone) url.setPath("/setZone"); QByteArray content; QXmlStreamWriter xml(&content); - xml.writeStartDocument(); + xml.writeStartDocument("1.0"); xml.writeStartElement("zone"); xml.writeAttribute("master", zone.deviceID); foreach (MemberObject member, zone.members){ @@ -293,8 +283,8 @@ void SoundTouch::setZone(ZoneObject zone) xml.writeEndElement(); //zone xml.writeEndDocument(); qDebug(dcBose) << "Sending request" << url << content; - //QNetworkReply *reply = m_networkAccessManager->post(QNetworkRequest(url), content); - //connect(reply, &QNetworkReply::finished, this, &SoundTouch::onRestRequestFinished); + QNetworkReply *reply = m_networkAccessManager->post(QNetworkRequest(url), content); + connect(reply, &QNetworkReply::finished, this, &SoundTouch::onRestRequestFinished); } void SoundTouch::addZoneSlave(ZoneObject zone) @@ -306,7 +296,7 @@ void SoundTouch::addZoneSlave(ZoneObject zone) url.setPath("/addZoneSlave"); QByteArray content; QXmlStreamWriter xml(&content); - xml.writeStartDocument(); + xml.writeStartDocument("1.0"); xml.writeStartElement("zone"); xml.writeAttribute("master", zone.deviceID); foreach (MemberObject member, zone.members){ @@ -316,8 +306,8 @@ void SoundTouch::addZoneSlave(ZoneObject zone) xml.writeEndElement(); //zone xml.writeEndDocument(); qDebug(dcBose) << "Sending request" << url << content; - //QNetworkReply *reply = m_networkAccessManager->post(QNetworkRequest(url), content); - //connect(reply, &QNetworkReply::finished, this, &SoundTouch::onRestRequestFinished); + QNetworkReply *reply = m_networkAccessManager->post(QNetworkRequest(url), content); + connect(reply, &QNetworkReply::finished, this, &SoundTouch::onRestRequestFinished); } void SoundTouch::removeZoneSlave(ZoneObject zone) @@ -339,8 +329,8 @@ void SoundTouch::removeZoneSlave(ZoneObject zone) xml.writeEndElement(); //zone xml.writeEndDocument(); qDebug(dcBose) << "Sending request" << url << content; - //QNetworkReply *reply = m_networkAccessManager->post(QNetworkRequest(url), content); - //connect(reply, &QNetworkReply::finished, this, &SoundTouch::onRestRequestFinished); + QNetworkReply *reply = m_networkAccessManager->post(QNetworkRequest(url), content); + connect(reply, &QNetworkReply::finished, this, &SoundTouch::onRestRequestFinished); } void SoundTouch::setBass(int volume) @@ -395,8 +385,8 @@ void SoundTouch::setSpeaker(PlayInfoObject playInfo) xml.writeEndElement(); //play_info xml.writeEndDocument(); qDebug(dcBose) << "Sending request" << url << content; - //QNetworkReply *reply = m_networkAccessManager->post(QNetworkRequest(url), content); - //connect(reply, &QNetworkReply::finished, this, &SoundTouch::onRestRequestFinished); + QNetworkReply *reply = m_networkAccessManager->post(QNetworkRequest(url), content); + connect(reply, &QNetworkReply::finished, this, &SoundTouch::onRestRequestFinished); } void SoundTouch::onWebsocketConnected() @@ -409,7 +399,7 @@ void SoundTouch::onWebsocketDisconnected() { qDebug(dcBose) << "Bose websocket disconnected"; emit connectionChanged(false); - QTimer::singleShot(5000, [this](){ + QTimer::singleShot(5000, this, [this](){ QUrl url; url.setHost(m_ipAddress); url.setScheme("ws"); @@ -423,15 +413,23 @@ void SoundTouch::onRestRequestFinished() { QNetworkReply *reply = static_cast(sender()); reply->deleteLater(); - QByteArray data = reply->readAll(); int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); - //qDebug(dcBose) << data; + if (m_getRequestQueue.isEmpty()) { + m_getRepliesPending = false; + } else { + sendGetRequest(m_getRequestQueue.takeFirst()); + } + // Check HTTP status code if (status != 200 || reply->error() != QNetworkReply::NoError) { - qCWarning(dcBose()) << "Request error:" << status << reply->errorString(); + qCWarning(dcBose()) << "Request error:" << reply->errorString() << "request:" << reply->url().path(); + return; } + QByteArray data = reply->readAll(); + //qDebug(dcBose) << data; + QXmlStreamReader xml; xml.addData(data); @@ -779,3 +777,19 @@ void SoundTouch::onWebsocketMessageReceived(QString message) qDebug(dcBose) << "Websocket message received:" << message; } + +void SoundTouch::sendGetRequest(QString path) +{ + QUrl url; + url.setHost(m_ipAddress); + url.setScheme("http"); + url.setPort(m_port); + url.setPath(path); + //qDebug(dcBose) << "Sending request" << url; + QNetworkRequest request = QNetworkRequest(url); + + QNetworkReply *reply = m_networkAccessManager->get(request); + m_getRepliesPending = true; + connect(reply, &QNetworkReply::finished, this, &SoundTouch::onRestRequestFinished); + connect(reply, SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(onRestRequestError(QNetworkReply::NetworkError))); +} diff --git a/bose/soundtouch.h b/bose/soundtouch.h index 887e5fc7..7ee30872 100644 --- a/bose/soundtouch.h +++ b/bose/soundtouch.h @@ -62,6 +62,18 @@ public: private: + enum RequestType { + Get, + Post + }; + + void sendGetRequest(QString path); + //get calls are getting queued to don't overstrain the device + //post calls must get sent immediately + //if an get call of the same URL is already in the queu the new one will be ignored + QList m_getRequestQueue; + bool m_getRepliesPending = false; + NetworkAccessManager *m_networkAccessManager = nullptr; QString m_ipAddress; int m_port = 8090; From a9de4281277329e645b2fdd3ea2adab79cea63b0 Mon Sep 17 00:00:00 2001 From: nymea Date: Fri, 26 Jul 2019 13:32:51 +0200 Subject: [PATCH 8/9] added plug-in to debian folder --- debian/control | 16 ++++++++++++++++ debian/nymea-plugin-bose.install.in | 1 + 2 files changed, 17 insertions(+) create mode 100644 debian/nymea-plugin-bose.install.in diff --git a/debian/control b/debian/control index 109179e7..27152e06 100644 --- a/debian/control +++ b/debian/control @@ -84,6 +84,21 @@ Description: nymea.io plugin for boblight for informations on boblight. +Package: nymea-plugin-bose +Architecture: any +Depends: ${shlibs:Depends}, + ${misc:Depends}, + nymea-plugins-translations, +Description: nymea.io plugin for bose soundtouch + 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 bose soundtouch. + + Package: nymea-plugin-commandlauncher Architecture: any Depends: ${shlibs:Depends}, @@ -724,6 +739,7 @@ Package: nymea-plugins Section: libs Architecture: all Depends: nymea-plugin-awattar, + nymea-plugin-bose, nymea-plugin-datetime, nymea-plugin-daylightsensor, nymea-plugin-denon, diff --git a/debian/nymea-plugin-bose.install.in b/debian/nymea-plugin-bose.install.in new file mode 100644 index 00000000..56ed1fa2 --- /dev/null +++ b/debian/nymea-plugin-bose.install.in @@ -0,0 +1 @@ +usr/lib/@DEB_HOST_MULTIARCH@/nymea/plugins/libnymea_devicepluginbose.so From 13e65b795ba6694b28ccb57b63550e05abb53820 Mon Sep 17 00:00:00 2001 From: Michael Zanetti Date: Sun, 28 Jul 2019 01:03:36 +0200 Subject: [PATCH 9/9] Add .ts file --- bose/translations/472a3f24-b05c-49b3-ad9a-dfda608b6760-en_US.ts | 1 + 1 file changed, 1 insertion(+) create mode 100644 bose/translations/472a3f24-b05c-49b3-ad9a-dfda608b6760-en_US.ts diff --git a/bose/translations/472a3f24-b05c-49b3-ad9a-dfda608b6760-en_US.ts b/bose/translations/472a3f24-b05c-49b3-ad9a-dfda608b6760-en_US.ts new file mode 100644 index 00000000..dcd02922 --- /dev/null +++ b/bose/translations/472a3f24-b05c-49b3-ad9a-dfda608b6760-en_US.ts @@ -0,0 +1 @@ + \ No newline at end of file