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"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+}