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..36862c76
--- /dev/null
+++ b/bose/devicepluginbose.cpp
@@ -0,0 +1,328 @@
+/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+ * *
+ * 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);
+ connect(soundTouch, &SoundTouch::bassReceived, this, &DevicePluginBose::onBassObjectReceived);
+ connect(soundTouch, &SoundTouch::bassCapabilitiesReceived, this, &DevicePluginBose::onBassCapabilitiesObjectReceived);
+ connect(soundTouch, &SoundTouch::zoneReceived, this, &DevicePluginBose::onZoneObjectReceived);
+
+ soundTouch->getInfo();
+ soundTouch->getNowPlaying();
+ soundTouch->getVolume();
+ soundTouch->getSources();
+ soundTouch->getBass();
+ soundTouch->getBassCapabilities();
+ soundTouch->getZone();
+
+ m_soundTouch.insert(device, soundTouch);
+
+ return Device::DeviceSetupStatusSuccess;
+ }
+ return Device::DeviceSetupStatusFailure;
+}
+
+void DevicePluginBose::deviceRemoved(Device *device)
+{
+ if (device->deviceClassId() == soundtouchDeviceClassId) {
+ SoundTouch *soundTouch = m_soundTouch.take(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;
+
+ 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);
+ 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() == 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") {
+ 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();
+ soundTouch->getBass();
+ soundTouch->getZone();
+ }
+}
+
+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(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);
+ 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;
+ }
+}
+
+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;
+}
+
+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
new file mode 100644
index 00000000..48e953b0
--- /dev/null
+++ b/bose/devicepluginbose.h
@@ -0,0 +1,71 @@
+/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+ * *
+ * 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);
+ void onBassObjectReceived(BassObject bass);
+ void onBassCapabilitiesObjectReceived(BassCapabilitiesObject bassCapabilities);
+ void onGroupObjectReceived(GroupObject group);
+ void onZoneObjectReceived(ZoneObject zone);
+};
+
+#endif // DEVICEPLUGINBOSE_H
diff --git a/bose/devicepluginbose.json b/bose/devicepluginbose.json
new file mode 100644
index 00000000..f1c911c7
--- /dev/null
+++ b/bose/devicepluginbose.json
@@ -0,0 +1,207 @@
+{
+ "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
+ },
+ {
+ "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"
+ },
+ {
+ "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": "Skip Next"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+}
diff --git a/bose/soundtouch.cpp b/bose/soundtouch.cpp
new file mode 100644
index 00000000..24ed1f54
--- /dev/null
+++ b/bose/soundtouch.cpp
@@ -0,0 +1,795 @@
+/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+ * *
+ * Copyright (C) 2019 Bernhard Trinnes . *
+ * *
+ * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
+
+#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"
+ //Seems QWebsockets doesn't support subprotocols
+ m_websocket->open(url);
+}
+
+void SoundTouch::getInfo()
+{
+ if (!m_getRepliesPending) {
+ sendGetRequest("/info");
+ } else {
+ if (!m_getRequestQueue.contains("/info"))
+ m_getRequestQueue.append("/info");
+ }
+}
+
+void SoundTouch::getVolume()
+{
+ if (!m_getRepliesPending) {
+ sendGetRequest("/volume");
+ } else {
+ if (!m_getRequestQueue.contains("/volume"))
+ m_getRequestQueue.append("/volume");
+ }
+}
+
+void SoundTouch::getNowPlaying()
+{
+ if (!m_getRepliesPending) {
+ sendGetRequest("/now_playing");
+ } else {
+ if (!m_getRequestQueue.contains("/now_playing"))
+ m_getRequestQueue.append("/now_playing");
+ }
+}
+
+void SoundTouch::getBass()
+{
+ if (!m_getRepliesPending) {
+ sendGetRequest("/bass");
+ } else {
+ if (!m_getRequestQueue.contains("/bass"))
+ m_getRequestQueue.append("/bass");
+ }
+}
+
+void SoundTouch::getGroup()
+{
+ if (!m_getRepliesPending) {
+ sendGetRequest("/getGroup");
+ } else {
+ if (!m_getRequestQueue.contains("/getGroup"))
+ m_getRequestQueue.append("/getGroup");
+ }
+}
+
+void SoundTouch::getSources()
+{
+ if (!m_getRepliesPending) {
+ sendGetRequest("/sources");
+ } else {
+ if (!m_getRequestQueue.contains("/sources"))
+ m_getRequestQueue.append("/sources");
+ }
+}
+
+void SoundTouch::getZone()
+{
+ if (!m_getRepliesPending) {
+ sendGetRequest("/getZone");
+ } else {
+ if (!m_getRequestQueue.contains("/getZone"))
+ m_getRequestQueue.append("/getZone");
+ }
+}
+
+void SoundTouch::getPresets()
+{
+ if (!m_getRepliesPending) {
+ sendGetRequest("/presets");
+ } else {
+ if (!m_getRequestQueue.contains("/presets"))
+ m_getRequestQueue.append("/presets");
+ }
+}
+
+
+void SoundTouch::getBassCapabilities()
+{
+ if (!m_getRepliesPending) {
+ sendGetRequest("/bassCapabilities");
+ } else {
+ if (!m_getRequestQueue.contains("/bassCapabilities"))
+ m_getRequestQueue.append("/bassCapabilities");
+ }
+}
+
+void SoundTouch::setKey(KEY_VALUE keyValue)
+{
+ QUrl url;
+ url.setHost(m_ipAddress);
+ url.setScheme("http");
+ url.setPort(m_port);
+ url.setPath("/key");
+ 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:
+ xml.writeCharacters("PLAY");
+ break;
+ case KEY_VALUE_STOP:
+ xml.writeCharacters("STOP");
+ break;
+ case KEY_VALUE_PAUSE:
+ xml.writeCharacters("PAUSE");
+ break;
+ case KEY_VALUE_PLAY_PAUSE:
+ xml.writeCharacters("PLAY_PAUSE");
+ break;
+ case KEY_VALUE_POWER:
+ xml.writeCharacters("POWER");
+ break;
+ case KEY_VALUE_NEXT_TRACK:
+ xml.writeCharacters("NEXT_TRACK");
+ break;
+ case KEY_VALUE_PREV_TRACK:
+ xml.writeCharacters("PREV_TRACK");
+ break;
+ case KEY_VALUE_BOOKMARK:
+ xml.writeCharacters("BOOKMARK");
+ break;
+ case KEY_VALUE_AUX_INPUT:
+ xml.writeCharacters("AUX_INPUT");
+ break;
+ case KEY_VALUE_REPEAT_ALL:
+ xml.writeCharacters("REPEAT_ALL");
+ break;
+ case KEY_VALUE_REPEAT_ONE:
+ xml.writeCharacters("REPEAT_ONE");
+ break;
+ case KEY_VALUE_REPEAT_OFF:
+ xml.writeCharacters("REPEAT_OFF");
+ break;
+ case KEY_VALUE_ADD_FAVORITE:
+ xml.writeCharacters("ADD_FAVORITE");
+ break;
+ case KEY_VALUE_MUTE:
+ xml.writeCharacters("MUTE");
+ break;
+ case KEY_VALUE_SHUFFLE_ON:
+ xml.writeCharacters("SHUFFLE_ON");
+ break;
+ case KEY_VALUE_SHUFFLE_OFF:
+ xml.writeCharacters("SHUFFLE_OFF");
+ break;
+ default:
+ qWarning(dcBose) << "key not yet implemented";
+ return;
+ }
+ 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);
+
+ if (keyValue == KEY_VALUE_POWER) {
+ QUrl url;
+ url.setHost(m_ipAddress);
+ url.setScheme("http");
+ url.setPort(m_port);
+ url.setPath("/key");
+ 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);
+ }
+}
+
+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::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("1.0");
+ xml.writeStartElement("zone");
+ xml.writeAttribute("master", zone.deviceID);
+ 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;
+ 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("1.0");
+ xml.writeStartElement("zone");
+ xml.writeAttribute("master", zone.deviceID);
+ 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;
+ 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);
+ 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;
+ 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);
+}
+
+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::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";
+ emit connectionChanged(true);
+}
+
+void SoundTouch::onWebsocketDisconnected()
+{
+ qDebug(dcBose) << "Bose websocket disconnected";
+ emit connectionChanged(false);
+ QTimer::singleShot(5000, this, [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();
+
+ int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
+
+ 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:" << reply->errorString() << "request:" << reply->url().path();
+ return;
+ }
+
+ QByteArray data = reply->readAll();
+ //qDebug(dcBose) << data;
+
+ QXmlStreamReader xml;
+ xml.addData(data);
+
+ if (xml.readNextStartElement()) {
+ if (xml.name() == "info") {
+ InfoObject info;
+ 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"){
+ ComponentObject component;
+ while(xml.readNextStartElement()){
+ if(xml.name() == "softwareVersion"){
+ //qDebug(dcBose) << "Software version" << xml.readElementText();
+ component.softwareVersion = xml.readElementText();
+ } else if(xml.name() == "serialNumber") {
+ //qDebug(dcBose) << "Serialnumber" << xml.readElementText();
+ component.serialNumber = xml.readElementText();
+ } else {
+ xml.skipCurrentElement();
+ }
+ }
+ info.components.append(component);
+ } else {
+ xml.skipCurrentElement();
+ }
+ }
+ } else if(xml.name() == "networkInfo"){
+ while (xml.readNextStartElement()) {
+ if (xml.name() == "macAddress") {
+ info.networkInfo.macAddress = xml.readElementText();
+ } else if(xml.name() == "ipAddress") {
+ info.networkInfo.ipAddress = xml.readElementText();
+ } else {
+ xml.skipCurrentElement();
+ }
+ }
+ } else {
+ xml.skipCurrentElement();
+ }
+ }
+ emit infoReceived(info);
+ } 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 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 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;
+
+ 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;
+ 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")) {
+ 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 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;
+
+}
+
+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
new file mode 100644
index 00000000..7ee30872
--- /dev/null
+++ b/bose/soundtouch.h
@@ -0,0 +1,104 @@
+/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+ * *
+ * Copyright (C) 2019 Bernhard Trinnes . *
+ * *
+ * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
+
+#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 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:
+ 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;
+ QWebSocket *m_websocket = nullptr;
+
+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();
+ void onRestRequestError(QNetworkReply::NetworkError error);
+};
+
+#endif // SOUNDTOUCH_H
diff --git a/bose/soundtouchtypes.h b/bose/soundtouchtypes.h
new file mode 100644
index 00000000..930d1ce1
--- /dev/null
+++ b/bose/soundtouchtypes.h
@@ -0,0 +1,250 @@
+/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+ * *
+ * Copyright (C) 2019 Bernhard Trinnes . *
+ * *
+ * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
+
+#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;
+ QList sourceItems;
+};
+
+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.
+ QList members; //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.
+ 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.
+};
+
+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
+
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
diff --git a/debian/control b/debian/control
index b83c4a60..37287fe8 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},
@@ -739,6 +754,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
diff --git a/nymea-plugins.pro b/nymea-plugins.pro
index b3ded9b2..75c495d4 100644
--- a/nymea-plugins.pro
+++ b/nymea-plugins.pro
@@ -5,6 +5,7 @@ PLUGIN_DIRS = \
avahimonitor \
awattar \
boblight \
+ bose \
commandlauncher \
conrad \
datetime \