diff --git a/bluos/README.md b/bluos/README.md
new file mode 100644
index 00000000..92a0f321
--- /dev/null
+++ b/bluos/README.md
@@ -0,0 +1,24 @@
+# BluOS
+
+This integration allows to control audio devices based on BluOS. BluOS is an operating system that can be found in products from Blusound, NAD Electronics, DALI Loudspeakers and others.
+
+## Supported Things
+
+* BluOS Player
+ * Multimedia control
+ * Volume
+ * Browsing Presets
+ * Browsing various music services
+ * Grouping speakers
+ * No internet or cloud connection required
+
+## Requirements
+
+* The BluOS device must be in the same local area network as nymea.
+* TCP sockets on port 80 must not be blocked by the router.
+* Blusound App to setup the speaker.
+* The package "nymea-plugin-bluos" must be installed
+
+## More
+
+https://www.bluesound.com/
diff --git a/bluos/bluos.cpp b/bluos/bluos.cpp
new file mode 100644
index 00000000..eea5df2d
--- /dev/null
+++ b/bluos/bluos.cpp
@@ -0,0 +1,701 @@
+/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+*
+* Copyright 2013 - 2020, nymea GmbH
+* Contact: contact@nymea.io
+*
+* This file is part of nymea.
+* This project including source code and documentation is protected by
+* copyright law, and remains the property of nymea GmbH. All rights, including
+* reproduction, publication, editing and translation, are reserved. The use of
+* this project is subject to the terms of a license agreement to be concluded
+* with nymea GmbH in accordance with the terms of use of nymea GmbH, available
+* under https://nymea.io/license
+*
+* GNU Lesser General Public License Usage
+* Alternatively, this project may be redistributed and/or modified under the
+* terms of the GNU Lesser General Public License as published by the Free
+* Software Foundation; version 3. This project is distributed in the hope that
+* it will be useful, but WITHOUT ANY WARRANTY; without even the implied
+* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+* Lesser General Public License for more details.
+*
+* You should have received a copy of the GNU Lesser General Public License
+* along with this project. If not, see .
+*
+* For any further details and any questions please contact us under
+* contact@nymea.io or see our FAQ/Licensing Information on
+* https://nymea.io/license/faq
+*
+* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
+
+#include "bluos.h"
+#include "extern-plugininfo.h"
+
+#include
+#include
+#include
+#include
+
+BluOS::BluOS(NetworkAccessManager *networkmanager, QHostAddress hostAddress, int port, QObject *parent) :
+ QObject(parent),
+ m_hostAddress(hostAddress),
+ m_port(port),
+ m_networkManager(networkmanager)
+{
+
+}
+
+int BluOS::port()
+{
+ return m_port;
+}
+
+QHostAddress BluOS::hostAddress()
+{
+ return m_hostAddress;
+}
+
+void BluOS::getStatus()
+{
+ QUrl url;
+ url.setScheme("http");
+ url.setHost(m_hostAddress.toString());
+ url.setPort(m_port);
+ url.setPath("/Status");
+ QNetworkReply *reply = m_networkManager->get(QNetworkRequest(url));
+ connect(reply, &QNetworkReply::finished, reply, &QNetworkReply::deleteLater);
+ connect(reply, &QNetworkReply::finished, this, [reply, this] {
+ int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
+ // Check HTTP status code
+ if (status != 200 || reply->error() != QNetworkReply::NoError) {
+ if (reply->error() == QNetworkReply::HostNotFoundError) {
+ emit connectionChanged(false);
+ }
+ qCWarning(dcBluOS()) << "Request error:" << status << reply->errorString();
+ return;
+ }
+ emit connectionChanged(true);
+ QByteArray data = reply->readAll();
+ //qCDebug(dcBluOS()) << "Get Status:" << data;
+ parseState(data);
+ });
+ return;
+}
+
+QUuid BluOS::setVolume(uint volume)
+{
+ QUuid requestId = QUuid::createUuid();
+
+ QUrlQuery query;
+ query.addQueryItem("level", QString::number(volume));
+ query.addQueryItem("tell_slaves", "off");
+
+ QUrl url;
+ url.setScheme("http");
+ url.setHost(m_hostAddress.toString());
+ url.setPort(m_port);
+ url.setPath("/Volume");
+ url.setQuery(query);
+ QNetworkReply *reply = m_networkManager->get(QNetworkRequest(url));
+ connect(reply, &QNetworkReply::finished, reply, &QNetworkReply::deleteLater);
+ connect(reply, &QNetworkReply::finished, this, [requestId, reply, this] {
+
+ int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
+
+ // Check HTTP status code
+ if (status != 200 || reply->error() != QNetworkReply::NoError) {
+ if (reply->error() == QNetworkReply::HostNotFoundError) {
+ emit connectionChanged(false);
+ }
+ emit actionExecuted(requestId, false);
+ qCWarning(dcBluOS()) << "Request error:" << status << reply->errorString();
+ return;
+ }
+ emit connectionChanged(true);
+
+ QXmlStreamReader xml;
+ xml.addData(reply->readAll());
+ if (xml.hasError()) {
+ qCDebug(dcBluOS()) << "XML Error:" << xml.errorString();
+ }
+ int volume = 0;
+ bool mute = false;
+ if (xml.readNextStartElement()) {
+ if (xml.name() == "volume") {
+ if(xml.attributes().hasAttribute("mute")) {
+ mute = xml.attributes().value("mute").toInt();
+ }
+ volume = xml.readElementText().toInt();
+ }
+ }
+ emit volumeReceived(volume, mute);
+ emit actionExecuted(requestId, true);
+ });
+ return requestId;
+}
+
+QUuid BluOS::setMute(bool mute)
+{
+ QUuid requestId = QUuid::createUuid();
+
+ QUrlQuery query;
+ query.addQueryItem("mute", QString::number(mute));
+ query.addQueryItem("tell_slaves", "off");
+
+ QUrl url;
+ url.setScheme("http");
+ url.setHost(m_hostAddress.toString());
+ url.setPort(m_port);
+ url.setPath("/Volume");
+ url.setQuery(query);
+
+ QNetworkReply *reply = m_networkManager->get(QNetworkRequest(url));
+ connect(reply, &QNetworkReply::finished, reply, &QNetworkReply::deleteLater);
+ connect(reply, &QNetworkReply::finished, this, [requestId, reply, this] {
+
+ int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
+
+ // Check HTTP status code
+ if (status != 200 || reply->error() != QNetworkReply::NoError) {
+ if (reply->error() == QNetworkReply::HostNotFoundError) {
+ emit connectionChanged(false);
+ }
+ emit actionExecuted(requestId, false);
+ qCWarning(dcBluOS()) << "Request error:" << status << reply->errorString();
+ return;
+ }
+ emit connectionChanged(true);
+ emit actionExecuted(requestId, true);
+ });
+
+ return requestId;
+}
+
+QUuid BluOS::play()
+{
+ return playBackControl(PlaybackCommand::Play);
+}
+
+QUuid BluOS::pause()
+{
+ return playBackControl(PlaybackCommand::Pause);
+}
+
+QUuid BluOS::stop()
+{
+ return playBackControl(PlaybackCommand::Stop);
+}
+
+QUuid BluOS::back()
+{
+ return playBackControl(PlaybackCommand::Back);
+}
+
+QUuid BluOS::setShuffle(bool shuffle)
+{
+ Q_UNUSED(shuffle)
+ QUuid requestId = QUuid::createUuid();
+
+ QUrl url;
+ url.setScheme("http");
+ url.setHost(m_hostAddress.toString());
+ url.setPort(m_port);
+ url.setPath("/Shuffle");
+ QUrlQuery query;
+ query.addQueryItem("state", QString::number(shuffle));
+ url.setQuery(query);
+ QNetworkReply *reply = m_networkManager->get(QNetworkRequest(url));
+ connect(reply, &QNetworkReply::finished, reply, &QNetworkReply::deleteLater);
+ connect(reply, &QNetworkReply::finished, this, [requestId, reply, this] {
+
+ int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
+
+ // Check HTTP status code
+ if (status != 200 || reply->error() != QNetworkReply::NoError) {
+ if (reply->error() == QNetworkReply::HostNotFoundError) {
+ emit connectionChanged(false);
+ }
+ emit actionExecuted(requestId, false);
+ qCWarning(dcBluOS()) << "Request error:" << status << reply->errorString();
+ return;
+ }
+ emit connectionChanged(true);
+ emit actionExecuted(requestId, true);
+
+ QXmlStreamReader xml;
+ xml.addData(reply->readAll());
+ if (xml.hasError()) {
+ qCDebug(dcBluOS()) << "XML Error:" << xml.errorString();
+ return;
+ }
+ if (xml.readNextStartElement()) {
+ if (xml.attributes().hasAttribute("shuffle")) {
+ bool shuffle = RepeatMode(xml.attributes().value("shuffle").toInt());
+ emit shuffleStateReceived(shuffle);
+ }
+ }
+ });
+ return requestId;
+}
+
+QUuid BluOS::setRepeat(RepeatMode repeatMode)
+{
+ Q_UNUSED(repeatMode)
+ QUuid requestId = QUuid::createUuid();
+
+ QUrl url;
+ url.setScheme("http");
+ url.setHost(m_hostAddress.toString());
+ url.setPort(m_port);
+ url.setPath("/Repeat");
+ QUrlQuery query;
+ query.addQueryItem("state", QString::number(repeatMode));
+ url.setQuery(query);
+ QNetworkReply *reply = m_networkManager->get(QNetworkRequest(url));
+ connect(reply, &QNetworkReply::finished, reply, &QNetworkReply::deleteLater);
+ connect(reply, &QNetworkReply::finished, this, [requestId, reply, this] {
+
+ int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
+
+ // Check HTTP status code
+ if (status != 200 || reply->error() != QNetworkReply::NoError) {
+ if (reply->error() == QNetworkReply::HostNotFoundError) {
+ emit connectionChanged(false);
+
+ }
+ emit actionExecuted(requestId, false);
+ qCWarning(dcBluOS()) << "Request error:" << status << reply->errorString();
+ return;
+ }
+ emit connectionChanged(true);
+ emit actionExecuted(requestId, true);
+
+ QXmlStreamReader xml;
+ xml.addData(reply->readAll());
+ if (xml.hasError()) {
+ qCDebug(dcBluOS()) << "XML Error:" << xml.errorString();
+ return;
+ }
+ if (xml.readNextStartElement()) {
+ if (xml.name() == "playlist") {
+ if (xml.attributes().hasAttribute("repeat")) {
+ RepeatMode mode = RepeatMode(xml.attributes().value("repeat").toInt());
+ emit repeatModeReceived(mode);
+ }
+ }
+ }
+ });
+ return requestId;
+}
+
+QUuid BluOS::listPresets()
+{
+ QUuid requestId = QUuid::createUuid();
+
+ QUrl url;
+ url.setScheme("http");
+ url.setHost(m_hostAddress.toString());
+ url.setPort(m_port);
+ url.setPath("/Presets");
+ QNetworkReply *reply = m_networkManager->get(QNetworkRequest(url));
+ connect(reply, &QNetworkReply::finished, reply, &QNetworkReply::deleteLater);
+ connect(reply, &QNetworkReply::finished, this, [requestId, reply, this] {
+
+ int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
+
+ // Check HTTP status code
+ if (status != 200 || reply->error() != QNetworkReply::NoError) {
+ if (reply->error() == QNetworkReply::HostNotFoundError) {
+ emit connectionChanged(false);
+ }
+
+ qCWarning(dcBluOS()) << "Request error:" << status << reply->errorString();
+ return;
+ }
+ emit connectionChanged(true);
+
+ QByteArray data = reply->readAll();
+ QXmlStreamReader xml;
+ xml.addData(data);
+ if (xml.hasError()) {
+ qCDebug(dcBluOS()) << "XML Error:" << xml.errorString();
+ return;
+ }
+ QList presetList;
+ if (xml.readNextStartElement()) {
+ if (xml.name() == "presets") {
+ while(xml.readNextStartElement()){
+ if(xml.name() == "preset"){
+ Preset preset;
+ if (xml.attributes().hasAttribute("id")) {
+ preset.Id = xml.attributes().value("id").toInt();
+ }
+ if (xml.attributes().hasAttribute("name")) {
+ preset.Name = xml.attributes().value("name").toString();
+ }
+ if (xml.attributes().hasAttribute("url")) {
+ preset.Url = xml.attributes().value("url").toString();
+ }
+ qCDebug(dcBluOS()) << "Preset text" << xml.readElementText(); //apparently the text must be read so the xml parser recognises the next element
+ presetList.append(preset);
+ } else {
+ xml.skipCurrentElement();
+ }
+ }
+ }
+ }
+ emit presetsReceived(requestId, presetList);
+ });
+ return requestId;
+}
+
+QUuid BluOS::loadPreset(int preset)
+{
+ QUuid requestId = QUuid::createUuid();
+
+ QUrl url;
+ url.setScheme("http");
+ url.setHost(m_hostAddress.toString());
+ url.setPort(m_port);
+ url.setPath("/Preset");
+ QUrlQuery query;
+ query.addQueryItem("id", QString::number(preset));
+ url.setQuery(query);
+ qCDebug(dcBluOS()) << "Loading preset" << url.toString();
+ QNetworkReply *reply = m_networkManager->get(QNetworkRequest(url));
+ connect(reply, &QNetworkReply::finished, reply, &QNetworkReply::deleteLater);
+ connect(reply, &QNetworkReply::finished, this, [requestId, reply, this] {
+
+ int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
+
+ // Check HTTP status code
+ if (status != 200 || reply->error() != QNetworkReply::NoError) {
+ if (reply->error() == QNetworkReply::HostNotFoundError) {
+ emit connectionChanged(false);
+ }
+ emit actionExecuted(requestId, false);
+ qCWarning(dcBluOS()) << "Request error:" << status << reply->errorString();
+ return;
+ }
+ emit connectionChanged(true);
+ emit actionExecuted(requestId, true);
+ });
+ return requestId;
+}
+
+QUuid BluOS::getSources()
+{
+ QUuid requestId = QUuid::createUuid();
+
+ QUrl url;
+ url.setScheme("http");
+ url.setHost(m_hostAddress.toString());
+ url.setPort(m_port);
+ url.setPath("/Browse");
+ QNetworkReply *reply = m_networkManager->get(QNetworkRequest(url));
+ connect(reply, &QNetworkReply::finished, reply, &QNetworkReply::deleteLater);
+ connect(reply, &QNetworkReply::finished, this, [requestId, reply, this] {
+
+ int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
+
+ // Check HTTP status code
+ if (status != 200 || reply->error() != QNetworkReply::NoError) {
+ if (reply->error() == QNetworkReply::HostNotFoundError) {
+ emit connectionChanged(false);
+ }
+ qCWarning(dcBluOS()) << "Request error:" << status << reply->errorString();
+ return;
+ }
+ emit connectionChanged(true);
+ QByteArray data = reply->readAll();
+ qCDebug(dcBluOS()) << "Sources: " << data;
+ QXmlStreamReader xml;
+ xml.addData(data);
+ if (xml.hasError()) {
+ qCDebug(dcBluOS()) << "XML Error:" << xml.errorString();
+ return;
+ }
+ QList sourceList;
+ if (xml.readNextStartElement()) {
+ if (xml.name() == "browse") {
+ while(xml.readNextStartElement()){
+ if(xml.name() == "item"){
+ Source source;
+ if (xml.attributes().hasAttribute("text")) {
+ source.Text = xml.attributes().value("text").toString();
+ }
+ if (xml.attributes().hasAttribute("type")) {
+ source.Type = xml.attributes().value("type").toString();
+ }
+ if (xml.attributes().hasAttribute("browseKey")) {
+ source.BrowseKey = xml.attributes().value("browseKey").toString();
+ }
+ if (xml.attributes().hasAttribute("image")) {
+ source.Image = xml.attributes().value("image").toString();
+ }
+ qCDebug(dcBluOS()) << "Source text" << xml.readElementText();
+ sourceList.append(source);
+ } else {
+ xml.skipCurrentElement();
+ }
+ }
+ }
+ }
+ emit sourcesReceived(requestId, sourceList);
+ });
+ return requestId;
+}
+
+QUuid BluOS::browseSource(const QString &key)
+{
+ QUuid requestId = QUuid::createUuid();
+
+ QUrl url;
+ url.setScheme("http");
+ url.setHost(m_hostAddress.toString());
+ url.setPort(m_port);
+ url.setPath("/Browse");
+ QUrlQuery query;
+ query.addQueryItem("key", key);
+ url.setQuery(query);
+ QNetworkReply *reply = m_networkManager->get(QNetworkRequest(url));
+ connect(reply, &QNetworkReply::finished, reply, &QNetworkReply::deleteLater);
+ connect(reply, &QNetworkReply::finished, this, [requestId, reply, this] {
+
+ int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
+
+ // Check HTTP status code
+ if (status != 200 || reply->error() != QNetworkReply::NoError) {
+ if (reply->error() == QNetworkReply::HostNotFoundError) {
+ emit connectionChanged(false);
+ }
+ qCWarning(dcBluOS()) << "Request error:" << status << reply->errorString();
+ return;
+ }
+ emit connectionChanged(true);
+ QByteArray data = reply->readAll();
+ qCDebug(dcBluOS()) << "Browse result: " << data;
+ QXmlStreamReader xml;
+ xml.addData(data);
+ if (xml.hasError()) {
+ qCDebug(dcBluOS()) << "XML Error:" << xml.errorString();
+ return;
+ }
+ QList sourceList;
+ if (xml.readNextStartElement()) {
+ if (xml.name() == "browse") {
+ while(xml.readNextStartElement()){
+ if(xml.name() == "item"){
+ Source source;
+ if (xml.attributes().hasAttribute("text")) {
+ source.Text = xml.attributes().value("text").toString();
+ }
+ if (xml.attributes().hasAttribute("type")) {
+ source.Type = xml.attributes().value("type").toString();
+ }
+ if (xml.attributes().hasAttribute("browseKey")) {
+ source.BrowseKey = xml.attributes().value("browseKey").toString();
+ }
+ if (xml.attributes().hasAttribute("image")) {
+ source.Image = xml.attributes().value("image").toString();
+ }
+ qCDebug(dcBluOS()) << "Source text" << xml.readElementText();
+ sourceList.append(source);
+ } else {
+ xml.skipCurrentElement();
+ }
+ }
+ }
+ }
+ emit sourcesReceived(requestId, sourceList);
+ });
+ return requestId;
+}
+
+QUuid BluOS::addGroupPlayer(QHostAddress address, int port)
+{
+ Q_UNUSED(address)
+ Q_UNUSED(port)
+ QUuid requestId = QUuid::createUuid();
+
+ QUrl url;
+ url.setScheme("http");
+ url.setHost(m_hostAddress.toString());
+ url.setPort(m_port);
+ url.setPath("/AddSlave");
+ QUrlQuery query;
+ query.addQueryItem("slave", address.toString());
+ query.addQueryItem("port", QString::number(port));
+ url.setQuery(query);
+ QNetworkReply *reply = m_networkManager->get(QNetworkRequest(url));
+ connect(reply, &QNetworkReply::finished, reply, &QNetworkReply::deleteLater);
+ connect(reply, &QNetworkReply::finished, this, [reply, this] {
+
+ int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
+
+ // Check HTTP status code
+ if (status != 200 || reply->error() != QNetworkReply::NoError) {
+ if (reply->error() == QNetworkReply::HostNotFoundError) {
+ emit connectionChanged(false);
+ }
+
+ qCWarning(dcBluOS()) << "Request error:" << status << reply->errorString();
+ return;
+ }
+ emit connectionChanged(true);
+ });
+ return requestId;
+}
+
+QUuid BluOS::removeGroupPlayer(QHostAddress address, int port)
+{
+ Q_UNUSED(address)
+ Q_UNUSED(port)
+ QUuid requestId = QUuid::createUuid();
+
+ QUrl url;
+ url.setScheme("http");
+ url.setHost(m_hostAddress.toString());
+ url.setPort(m_port);
+ url.setPath("/RemoveSlave");
+ QUrlQuery query;
+ query.addQueryItem("slave", address.toString());
+ query.addQueryItem("port", QString::number(port));
+ url.setQuery(query);
+ QNetworkReply *reply = m_networkManager->get(QNetworkRequest(url));
+ connect(reply, &QNetworkReply::finished, reply, &QNetworkReply::deleteLater);
+ connect(reply, &QNetworkReply::finished, this, [reply, this] {
+
+ int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
+
+ // Check HTTP status code
+ if (status != 200 || reply->error() != QNetworkReply::NoError) {
+ if (reply->error() == QNetworkReply::HostNotFoundError) {
+ emit connectionChanged(false);
+ }
+
+ qCWarning(dcBluOS()) << "Request error:" << status << reply->errorString();
+ return;
+ }
+ emit connectionChanged(true);
+ });
+ return requestId;
+}
+
+QUuid BluOS::skip()
+{
+ return playBackControl(PlaybackCommand::Skip);
+}
+
+QUuid BluOS::playBackControl(BluOS::PlaybackCommand command)
+{
+ QUuid requestId = QUuid::createUuid();
+ QUrl url;
+ url.setScheme("http");
+ url.setHost(m_hostAddress.toString());
+ url.setPort(m_port);
+ switch (command) {
+ case PlaybackCommand::Play:
+ url.setPath("/Play");
+ break;
+ case PlaybackCommand::Pause:
+ url.setPath("/Pause");
+ break;
+ case PlaybackCommand::Stop:
+ url.setPath("/Stop");
+ break;
+ case PlaybackCommand::Back:
+ url.setPath("/Back");
+ break;
+ case PlaybackCommand::Skip:
+ url.setPath("/Skip");
+ break;
+ }
+ QNetworkRequest request;
+ request.setUrl(url);
+ QNetworkReply *reply = m_networkManager->get(request);
+ qCDebug(dcBluOS()) << "Sending request" << request.url();
+ connect(reply, &QNetworkReply::finished, reply, &QNetworkReply::deleteLater);
+ connect(reply, &QNetworkReply::finished, this, [requestId, reply, this] {
+
+ int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
+
+ // Check HTTP status code
+ if (status != 200 || reply->error() != QNetworkReply::NoError) {
+ if (reply->error() == QNetworkReply::HostNotFoundError) {
+ emit connectionChanged(false);
+ }
+ emit actionExecuted(requestId, false);
+ qCWarning(dcBluOS()) << "Request error:" << status << reply->errorString();
+ return;
+ }
+ emit connectionChanged(true);
+
+ QByteArray data = reply->readAll();
+ parseState(data);
+ });
+ return requestId;
+}
+
+bool BluOS::parseState(const QByteArray &state)
+{
+ QXmlStreamReader xml;
+ xml.addData(state);
+ if (xml.hasError()) {
+ qCDebug(dcBluOS()) << "XML Error:" << xml.errorString();
+ return false;
+ }
+
+ StatusResponse statusResponse;
+ if (xml.readNextStartElement()) {
+ if (xml.name() == "status") {
+ while(xml.readNextStartElement()){
+ if(xml.name() == "artist"){
+ statusResponse.Artist = xml.readElementText();
+ } else if(xml.name() == "album"){
+ statusResponse.Album = xml.readElementText();
+ } else if(xml.name() == "name"){
+ statusResponse.Name = xml.readElementText();
+ } else if(xml.name() == "service"){
+ statusResponse.Service = xml.readElementText();
+ } else if(xml.name() == "serviceIcon"){
+ statusResponse.ServiceIcon = xml.readElementText();
+ } else if(xml.name() == "shuffle"){
+ statusResponse.Shuffle = xml.readElementText().toInt();
+ } else if(xml.name() == "repeat"){
+ statusResponse.Shuffle = xml.readElementText().toInt();
+ } else if(xml.name() == "state"){
+ QString playback = xml.readElementText();
+ if (playback == "play") {
+ statusResponse.State = PlaybackState::Playing;
+ } else if (playback == "pause") {
+ statusResponse.State = PlaybackState::Paused;
+ } else if (playback == "stop") {
+ statusResponse.State = PlaybackState::Stopped;
+ } else if (playback == "connecting") {
+ statusResponse.State = PlaybackState::Connecting;
+ } else if (playback == "stream") {
+ statusResponse.State = PlaybackState::Streaming;
+ } else {
+ statusResponse.State = PlaybackState::Stopped;
+ qCWarning(dcBluOS()) << "State response, unhandled playback mode" << playback;
+ }
+ } else if(xml.name() == "volume"){
+ statusResponse.Volume = xml.readElementText().toInt();
+ } else if(xml.name() == "mute"){
+ statusResponse.Mute = xml.readElementText().toInt();
+ } else if(xml.name() == "image") {
+ statusResponse.Image = xml.readElementText();
+ } else if(xml.name() == "title1") {
+ statusResponse.Title = xml.readElementText();
+ } else if(xml.name() == "group") {
+ statusResponse.Group = xml.readElementText();
+ } else {
+ xml.skipCurrentElement();
+ }
+ }
+ }
+ }
+ emit statusReceived(statusResponse);
+ return true;
+}
diff --git a/bluos/bluos.h b/bluos/bluos.h
new file mode 100644
index 00000000..6263bd34
--- /dev/null
+++ b/bluos/bluos.h
@@ -0,0 +1,153 @@
+/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+*
+* Copyright 2013 - 2020, nymea GmbH
+* Contact: contact@nymea.io
+*
+* This file is part of nymea.
+* This project including source code and documentation is protected by
+* copyright law, and remains the property of nymea GmbH. All rights, including
+* reproduction, publication, editing and translation, are reserved. The use of
+* this project is subject to the terms of a license agreement to be concluded
+* with nymea GmbH in accordance with the terms of use of nymea GmbH, available
+* under https://nymea.io/license
+*
+* GNU Lesser General Public License Usage
+* Alternatively, this project may be redistributed and/or modified under the
+* terms of the GNU Lesser General Public License as published by the Free
+* Software Foundation; version 3. This project is distributed in the hope that
+* it will be useful, but WITHOUT ANY WARRANTY; without even the implied
+* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+* Lesser General Public License for more details.
+*
+* You should have received a copy of the GNU Lesser General Public License
+* along with this project. If not, see .
+*
+* For any further details and any questions please contact us under
+* contact@nymea.io or see our FAQ/Licensing Information on
+* https://nymea.io/license/faq
+*
+* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
+
+#ifndef BLUOS_H
+#define BLUOS_H
+
+#include
+#include
+#include
+#include
+
+#include "network/networkaccessmanager.h"
+#include "integrations/thing.h"
+
+class BluOS : public QObject
+{
+ Q_OBJECT
+public:
+
+ enum PlaybackCommand {
+ Play,
+ Pause,
+ Stop,
+ Skip,
+ Back
+ };
+
+ enum RepeatMode {
+ All,
+ One,
+ None
+ };
+
+ enum PlaybackState {
+ Playing,
+ Paused,
+ Stopped,
+ Connecting,
+ Streaming
+ };
+
+ struct StatusResponse {
+ QString Album;
+ QString Artist;
+ QString Name;
+ QString Title;
+ QString Service;
+ QUrl ServiceIcon;
+ PlaybackState State;
+ QUrl StationUrl;
+ int Volume;
+ bool Mute;
+ RepeatMode Repeat;
+ bool Shuffle;
+ QUrl Image;
+ QString Group;
+ };
+
+ struct Preset {
+ QString Name;
+ int Id;
+ QString Url;
+ };
+
+ struct Source {
+ //
+ QString Image;
+ QString BrowseKey;
+ QString Text;
+ QString Type;
+ };
+
+ explicit BluOS(NetworkAccessManager *networkManager, QHostAddress hostAddress, int port, QObject *parent = nullptr);
+ int port();
+ QHostAddress hostAddress();
+
+ // Status Queries
+ void getStatus();
+
+ // Volume Control
+ QUuid setVolume(uint volume);
+ QUuid setMute(bool mute);
+
+ // Playback Control
+ QUuid play();
+ QUuid pause();
+ QUuid stop();
+ QUuid skip();
+ QUuid back();
+ QUuid setShuffle(bool shuffle);
+ QUuid setRepeat(RepeatMode repeatMode);
+
+ // Presets
+ QUuid listPresets();
+ QUuid loadPreset(int preset); //1 for next preset, -1 for previous preset
+
+ // Content Browsing
+ QUuid getSources();
+ QUuid browseSource(const QString &key);
+
+ // Player Grouping
+ QUuid addGroupPlayer(QHostAddress address, int port); //adds player as slave
+ QUuid removeGroupPlayer(QHostAddress address, int port);
+
+private:
+ QHostAddress m_hostAddress;
+ int m_port;
+ NetworkAccessManager *m_networkManager = nullptr;
+
+ QUuid playBackControl(PlaybackCommand command);
+ bool parseState(const QByteArray &state);
+
+signals:
+ void connectionChanged(bool connected);
+ void actionExecuted(QUuid actionId, bool success);
+
+ void statusReceived(const StatusResponse &status);
+ void volumeReceived(int volume, bool mute);
+ void shuffleStateReceived(bool state);
+ void repeatModeReceived(RepeatMode mode);
+
+ void presetsReceived(QUuid requestId, const QList &presets);
+ void sourcesReceived(QUuid requestId, const QList &sources);
+ void browseResultReceived(QUuid requestId, const QList &sources);
+};
+#endif // BLUOS_H
diff --git a/bluos/bluos.pro b/bluos/bluos.pro
new file mode 100644
index 00000000..4297114e
--- /dev/null
+++ b/bluos/bluos.pro
@@ -0,0 +1,11 @@
+include(../plugins.pri)
+
+QT += network
+
+SOURCES += \
+ integrationpluginbluos.cpp \
+ bluos.cpp \
+
+HEADERS += \
+ integrationpluginbluos.h \
+ bluos.h \
diff --git a/bluos/blusound.png b/bluos/blusound.png
new file mode 100644
index 00000000..5477d894
Binary files /dev/null and b/bluos/blusound.png differ
diff --git a/bluos/integrationpluginbluos.cpp b/bluos/integrationpluginbluos.cpp
new file mode 100644
index 00000000..4b81efbf
--- /dev/null
+++ b/bluos/integrationpluginbluos.cpp
@@ -0,0 +1,568 @@
+/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+*
+* Copyright 2013 - 2020, nymea GmbH
+* Contact: contact@nymea.io
+*
+* This file is part of nymea.
+* This project including source code and documentation is protected by
+* copyright law, and remains the property of nymea GmbH. All rights, including
+* reproduction, publication, editing and translation, are reserved. The use of
+* this project is subject to the terms of a license agreement to be concluded
+* with nymea GmbH in accordance with the terms of use of nymea GmbH, available
+* under https://nymea.io/license
+*
+* GNU Lesser General Public License Usage
+* Alternatively, this project may be redistributed and/or modified under the
+* terms of the GNU Lesser General Public License as published by the Free
+* Software Foundation; version 3. This project is distributed in the hope that
+* it will be useful, but WITHOUT ANY WARRANTY; without even the implied
+* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+* Lesser General Public License for more details.
+*
+* You should have received a copy of the GNU Lesser General Public License
+* along with this project. If not, see .
+*
+* For any further details and any questions please contact us under
+* contact@nymea.io or see our FAQ/Licensing Information on
+* https://nymea.io/license/faq
+*
+* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
+
+
+#include "integrationpluginbluos.h"
+#include "plugininfo.h"
+#include "integrations/thing.h"
+#include "network/networkaccessmanager.h"
+#include "types/mediabrowseritem.h"
+#include "types/browseritem.h"
+
+#include
+#include
+#include
+#include
+
+
+IntegrationPluginBluOS::IntegrationPluginBluOS()
+{
+
+}
+
+void IntegrationPluginBluOS::init()
+{
+ m_serviceBrowser = hardwareManager()->zeroConfController()->createServiceBrowser("_musc._tcp");
+}
+
+void IntegrationPluginBluOS::discoverThings(ThingDiscoveryInfo *info)
+{
+ QTimer::singleShot(5000, info, [this, info](){
+ foreach (const ZeroConfServiceEntry avahiEntry, m_serviceBrowser->serviceEntries()) {
+ qCDebug(dcBluOS()) << "Zeroconf entry:" << avahiEntry;
+
+ QString playerId = avahiEntry.hostName().split(".").first();
+ ThingDescriptor descriptor(bluosPlayerThingClassId, avahiEntry.name(), avahiEntry.hostAddress().toString());
+ ParamList params;
+
+ foreach (Thing *existingDevice, myThings().filterByThingClassId(bluosPlayerThingClassId)) {
+ if (existingDevice->paramValue(bluosPlayerThingSerialNumberParamTypeId).toString() == playerId) {
+ descriptor.setThingId(existingDevice->id());
+ break;
+ }
+ }
+ params << Param(bluosPlayerThingAddressParamTypeId, avahiEntry.hostAddress().toString());
+ params << Param(bluosPlayerThingPortParamTypeId, avahiEntry.port());
+ params << Param(bluosPlayerThingSerialNumberParamTypeId, playerId);
+ descriptor.setParams(params);
+ info->addThingDescriptor(descriptor);
+ }
+ info->finish(Thing::ThingErrorNoError);
+ });
+}
+
+void IntegrationPluginBluOS::setupThing(ThingSetupInfo *info)
+{
+ Thing *thing = info->thing();
+
+ if (thing->thingClassId() == bluosPlayerThingClassId) {
+ qCDebug(dcBluOS()) << "Setup BluOS device" << thing->paramValue(bluosPlayerThingAddressParamTypeId).toString();
+
+ QHostAddress address(thing->paramValue(bluosPlayerThingAddressParamTypeId).toString());
+ int port = thing->paramValue(bluosPlayerThingPortParamTypeId).toInt();
+ BluOS *bluos = new BluOS(hardwareManager()->networkManager() , address, port, this);
+ connect(bluos, &BluOS::connectionChanged, this, &IntegrationPluginBluOS::onConnectionChanged);
+ connect(bluos, &BluOS::statusReceived, this, &IntegrationPluginBluOS::onStatusResponseReceived);
+ connect(bluos, &BluOS::actionExecuted, this, &IntegrationPluginBluOS::onActionExecuted);
+ connect(bluos, &BluOS::volumeReceived, this, &IntegrationPluginBluOS::onVolumeReceived);
+ connect(bluos, &BluOS::presetsReceived, this, &IntegrationPluginBluOS::onPresetsReceived);
+ connect(bluos, &BluOS::sourcesReceived, this, &IntegrationPluginBluOS::onSourcesReceived);
+ connect(bluos, &BluOS::shuffleStateReceived, this, &IntegrationPluginBluOS::onShuffleStateReceived);
+ connect(bluos, &BluOS::repeatModeReceived, this, &IntegrationPluginBluOS::onRepeatModeReceived);
+
+ m_asyncSetup.insert(bluos, info);
+ bluos->getStatus();
+ // In case the setup is cancelled before we finish it...
+ connect(info, &ThingSetupInfo::aborted, this, [this, bluos] {
+ m_asyncSetup.remove(bluos);
+ bluos->deleteLater();
+ });
+ return;
+ } else {
+ return info->finish(Thing::ThingErrorThingClassNotFound);
+ }
+}
+
+void IntegrationPluginBluOS::postSetupThing(Thing *thing)
+{
+ Q_UNUSED(thing)
+
+ if (!m_pluginTimer) {
+ m_pluginTimer = hardwareManager()->pluginTimerManager()->registerTimer(10);
+ connect(m_pluginTimer, &PluginTimer::timeout, [this] {
+ foreach(BluOS *bluos, m_bluos) {
+ bluos->getStatus();
+ }
+ });
+ }
+}
+
+void IntegrationPluginBluOS::thingRemoved(Thing *thing)
+{
+ if (thing->thingClassId() == bluosPlayerThingClassId) {
+ BluOS *bluos = m_bluos.take(thing->id());
+ bluos->deleteLater();
+ } else {
+ qCWarning(dcBluOS()) << "Things removed, unhandled thing class id";
+ }
+}
+
+void IntegrationPluginBluOS::executeAction(ThingActionInfo *info)
+{
+ Thing *thing = info->thing();
+ Action action = info->action();
+
+ if (thing->thingClassId() == bluosPlayerThingClassId) {
+ BluOS *bluos = m_bluos.value(thing->id());
+ if (!bluos) {
+ return info->finish(Thing::ThingErrorHardwareFailure);
+ }
+ if (action.actionTypeId() == bluosPlayerPlaybackStatusActionTypeId) {
+ QString playbakStatus = action.param(bluosPlayerPlaybackStatusEventPlaybackStatusParamTypeId).value().toString();
+ QUuid requestId;
+ if (playbakStatus == "Playing") {
+ requestId = bluos->play();
+ } else if (playbakStatus == "Paused") {
+ requestId = bluos->pause();
+ } else if (playbakStatus == "Stopped") {
+ requestId = bluos->stop();
+ } else {
+ qCWarning(dcBluOS()) << "Unhandled Playback mode";
+ }
+ m_asyncActions.insert(requestId, info);
+ connect(info, &ThingActionInfo::aborted, [this, requestId] {m_asyncActions.remove(requestId);});
+ } else if (action.actionTypeId() == bluosPlayerPlayActionTypeId) {
+ QUuid requestId = bluos->play();
+ m_asyncActions.insert(requestId, info);
+ connect(info, &ThingActionInfo::aborted, [this, requestId] {m_asyncActions.remove(requestId);});
+ } else if (action.actionTypeId() == bluosPlayerPauseActionTypeId) {
+ QUuid requestId = bluos->pause();
+ m_asyncActions.insert(requestId, info);
+ connect(info, &ThingActionInfo::aborted, [this, requestId] {m_asyncActions.remove(requestId);});
+ } else if (action.actionTypeId() == bluosPlayerStopActionTypeId) {
+ QUuid requestId = bluos->stop();
+ m_asyncActions.insert(requestId, info);
+ connect(info, &ThingActionInfo::aborted, [this, requestId] {m_asyncActions.remove(requestId);});
+ } else if (action.actionTypeId() == bluosPlayerSkipNextActionTypeId) {
+ QUuid requestId = bluos->skip();
+ m_asyncActions.insert(requestId, info);
+ connect(info, &ThingActionInfo::aborted, [this, requestId] {m_asyncActions.remove(requestId);});
+ } else if (action.actionTypeId() == bluosPlayerSkipBackActionTypeId) {
+ QUuid requestId = bluos->back();
+ m_asyncActions.insert(requestId, info);
+ connect(info, &ThingActionInfo::aborted, [this, requestId] {m_asyncActions.remove(requestId);});
+ } else if (action.actionTypeId() == bluosPlayerVolumeActionTypeId) {
+ uint volume = action.param(bluosPlayerVolumeActionVolumeParamTypeId).value().toUInt();
+ QUuid requestId = bluos->setVolume(volume);
+ m_asyncActions.insert(requestId, info);
+ connect(info, &ThingActionInfo::aborted, [this, requestId] {m_asyncActions.remove(requestId);});
+ } else if (action.actionTypeId() == bluosPlayerMuteActionTypeId) {
+ bool mute = action.param(bluosPlayerMuteActionMuteParamTypeId).value().toBool();
+ QUuid requestId = bluos->setMute(mute);
+ m_asyncActions.insert(requestId, info);
+ connect(info, &ThingActionInfo::aborted, [this, requestId] {m_asyncActions.remove(requestId);});
+ } else if (action.actionTypeId() == bluosPlayerShuffleActionTypeId) {
+ bool shuffle = action.param(bluosPlayerShuffleActionShuffleParamTypeId).value().toBool();
+ QUuid requestId = bluos->setShuffle(shuffle);
+ m_asyncActions.insert(requestId, info);
+ connect(info, &ThingActionInfo::aborted, [this, requestId] {m_asyncActions.remove(requestId);});
+ } else if (action.actionTypeId() == bluosPlayerRepeatActionTypeId) {
+ QString repeat = action.param(bluosPlayerRepeatActionRepeatParamTypeId).value().toString();
+ QUuid requestId;
+ if (repeat == "One") {
+ requestId = bluos->setRepeat(BluOS::RepeatMode::One);
+ } else if (repeat == "All") {
+ requestId = bluos->setRepeat(BluOS::RepeatMode::All);
+ } else if (repeat == "None") {
+ requestId = bluos->setRepeat(BluOS::RepeatMode::None);
+ } else {
+ qCWarning(dcBluOS()) << "Unhandled Repeat Mode";
+ }
+ m_asyncActions.insert(requestId, info);
+ connect(info, &ThingActionInfo::aborted, [this, requestId] {m_asyncActions.remove(requestId);});
+ } else {
+ qCWarning(dcBluOS()) << "Execute Action, unhandled action type id" << action.actionTypeId();
+ return info->finish(Thing::ThingErrorThingClassNotFound);
+ }
+ } else {
+ qCWarning(dcBluOS()) << "Execute Action, unhandled thing class id" << thing->thingClassId();
+ return info->finish(Thing::ThingErrorThingClassNotFound);
+ }
+}
+
+void IntegrationPluginBluOS::browseThing(BrowseResult *result)
+{
+ Thing *thing = result->thing();
+ if (thing->thingClassId() == bluosPlayerThingClassId) {
+ BluOS *bluos = m_bluos.value(thing->id());
+ if (!bluos) {
+ qCWarning(dcBluOS()) << "Could not find any BluOS object that belongs to" << thing->name();
+ result->finish(Thing::ThingErrorHardwareNotAvailable, "BluOS connection not properly initialized");
+ return;
+ }
+ if (result->itemId() == "presets") {
+ QUuid requestId = bluos->listPresets();
+ m_asyncBrowseResults.insert(requestId, result);
+ connect(result, &BrowseResult::aborted, this, [this, requestId]{m_asyncBrowseResults.remove(requestId);});
+ } else if (result->itemId() == "grouping") {
+ foreach (const ZeroConfServiceEntry avahiEntry, m_serviceBrowser->serviceEntries()) {
+ qCDebug(dcBluOS()) << "Zeroconf entry:" << avahiEntry;
+
+ QString playerId = avahiEntry.hostName().split(".").first();
+ if (thing->paramValue(bluosPlayerThingSerialNumberParamTypeId).toString() == playerId) {
+ continue;
+ }
+ MediaBrowserItem groupingItem("grouping&"+avahiEntry.hostAddress().toString()+"&"+avahiEntry.port(), avahiEntry.name(), true, false);
+ groupingItem.setDescription(avahiEntry.hostAddress().toString());
+ groupingItem.setMediaIcon(MediaBrowserItem::MediaBrowserIconNetwork);
+ groupingItem.setIcon(BrowserItem::BrowserIconMusic);
+ result->addItem(groupingItem);
+ }
+ } else if (result->itemId().isEmpty()) {
+ MediaBrowserItem presetItem("presets", "Presets", true, false);
+ presetItem.setIcon(BrowserItem::BrowserIcon::BrowserIconFavorites);
+ presetItem.setMediaIcon(MediaBrowserItem::MediaBrowserIconMusicLibrary);
+ result->addItem(presetItem);
+
+ // MediaBrowserItem groupingItem("grouping", "Grouping", true, false);
+ // groupingItem.setIcon(BrowserItem::BrowserIcon::BrowserIconApplication);
+ // groupingItem.setMediaIcon(MediaBrowserItem::MediaBrowserIconNetwork);
+ // result->addItem(groupingItem);
+
+ QUuid requestId = bluos->getSources();
+ m_asyncBrowseResults.insert(requestId, result);
+ connect(result, &BrowseResult::aborted, this, [this, requestId]{m_asyncBrowseResults.remove(requestId);});
+ } else {
+ QUuid requestId = bluos->browseSource(result->itemId());
+ m_asyncBrowseResults.insert(requestId, result);
+ connect(result, &BrowseResult::aborted, this, [this, requestId]{m_asyncBrowseResults.remove(requestId);});
+ }
+ }
+}
+
+void IntegrationPluginBluOS::browserItem(BrowserItemResult *result)
+{
+ Thing *thing = result->thing();
+ if (thing->thingClassId() == bluosPlayerThingClassId) {
+ BluOS *bluos = m_bluos.value(thing->id());
+ if (!bluos) {
+ qCWarning(dcBluOS()) << "Could not find any BluOS object that belongs to" << thing->name();
+ return;
+ }
+ if (result->itemId() == "presets") {
+ QUuid requestId = bluos->listPresets();
+ m_asyncBrowseItemResults.insert(requestId, result);
+ connect(result, &BrowserItemResult::aborted, this, [this, requestId]{m_asyncBrowseItemResults.remove(requestId);});
+ } else {
+ BrowserItem presetItem("presets", "Presets", true, false);
+ presetItem.setIcon(BrowserItem::BrowserIcon::BrowserIconFavorites);
+ QUuid requestId = bluos->getSources();
+ m_asyncBrowseItemResults.insert(requestId, result);
+ connect(result, &BrowserItemResult::aborted, this, [this, requestId]{m_asyncBrowseItemResults.remove(requestId);});
+ }
+ }
+}
+
+void IntegrationPluginBluOS::executeBrowserItem(BrowserActionInfo *info)
+{
+ Thing *thing = info->thing();
+ if (thing->thingClassId() == bluosPlayerThingClassId) {
+ BluOS *bluos = m_bluos.value(thing->id());
+ if (!bluos) {
+ qCWarning(dcBluOS()) << "Could not find any BluOS object that belongs to" << thing->name();
+ return;
+ }
+
+ if (info->browserAction().itemId().startsWith("presets")) {
+ QUuid requestId;
+ int presetId = info->browserAction().itemId().split("&").last().toInt();
+ requestId = bluos->loadPreset(presetId);
+ m_asyncExecuteBrowseItems.insert(requestId, info);
+ connect(info, &BrowserActionInfo::aborted, this, [this, requestId]{m_asyncExecuteBrowseItems.remove(requestId);});
+ } else if (info->browserAction().itemId().startsWith("grouping")) {
+ //TODO Grouping
+ //Test devices are required
+ } else {
+ //TODO Sources
+ //Test services are required
+ }
+ }
+}
+
+void IntegrationPluginBluOS::onConnectionChanged(bool connected)
+{
+ BluOS *bluos = static_cast(sender());
+
+ if (m_asyncSetup.contains(bluos)) {
+ ThingSetupInfo *info = m_asyncSetup.take(bluos);
+ if (connected) {
+ m_bluos.insert(info->thing()->id(), bluos);
+ info->thing()->setStateValue(bluosPlayerConnectedStateTypeId, true);
+ info->finish(Thing::ThingErrorNoError);
+ } else {
+ bluos->deleteLater();
+ info->finish(Thing::ThingErrorSetupFailed);
+ }
+ } else {
+ Thing *thing = myThings().findById(m_bluos.key(bluos));
+ if (!thing) {
+ qCWarning(dcBluOS()) << "Could not find any Thing that belongs to the BluOS object";
+ return;
+ }
+ thing->setStateValue(bluosPlayerConnectedStateTypeId, connected);
+ }
+}
+
+void IntegrationPluginBluOS::onStatusResponseReceived(const BluOS::StatusResponse &status)
+{
+ BluOS *bluos = static_cast(sender());
+ Thing *thing = myThings().findById(m_bluos.key(bluos));
+ if (!thing){
+ qCWarning(dcBluOS()) << "Could not find any Thing that belongs to this BluOS object";
+ return;
+ }
+ thing->setStateValue(bluosPlayerArtistStateTypeId, status.Artist);
+ thing->setStateValue(bluosPlayerCollectionStateTypeId, status.Album);
+ thing->setStateValue(bluosPlayerTitleStateTypeId, status.Title);
+ thing->setStateValue(bluosPlayerSourceStateTypeId, status.Service);
+ thing->setStateValue(bluosPlayerArtworkStateTypeId, status.Image);
+ switch (status.State) {
+ case BluOS::PlaybackState::Playing:
+ case BluOS::PlaybackState::Streaming:
+ thing->setStateValue(bluosPlayerPlaybackStatusStateTypeId, "Playing");
+ break;
+ case BluOS::PlaybackState::Paused:
+ thing->setStateValue(bluosPlayerPlaybackStatusStateTypeId, "Paused");
+ break;
+ case BluOS::PlaybackState::Stopped:
+ thing->setStateValue(bluosPlayerPlaybackStatusStateTypeId, "Stopped");
+ break;
+ default:
+ thing->setStateValue(bluosPlayerPlaybackStatusStateTypeId, "Stopped");
+ break;
+ }
+
+ thing->setStateValue(bluosPlayerMuteStateTypeId, status.Mute);
+ thing->setStateValue(bluosPlayerVolumeStateTypeId, status.Volume);
+ thing->setStateValue(bluosPlayerShuffleStateTypeId, status.Shuffle);
+ switch (status.Repeat) {
+ case BluOS::RepeatMode::All:
+ thing->setStateValue(bluosPlayerRepeatStateTypeId, "All");
+ break;
+ case BluOS::RepeatMode::One:
+ thing->setStateValue(bluosPlayerRepeatStateTypeId, "One");
+ break;
+ case BluOS::RepeatMode::None:
+ thing->setStateValue(bluosPlayerRepeatStateTypeId, "None");
+ break;
+ }
+ thing->setStateValue(bluosPlayerGroupStateTypeId, status.Group);
+}
+
+void IntegrationPluginBluOS::onActionExecuted(QUuid requestId, bool success)
+{
+ if (m_asyncActions.contains(requestId)) {
+ ThingActionInfo *info = m_asyncActions.take(requestId);
+ if (success) {
+ info->finish(Thing::ThingErrorNoError);
+ } else {
+ info->finish(Thing::ThingErrorHardwareNotAvailable);
+ }
+ }
+ if (m_asyncExecuteBrowseItems.contains(requestId)) {
+ BrowserActionInfo *info = m_asyncExecuteBrowseItems.take(requestId);
+ if (success) {
+ info->finish(Thing::ThingErrorNoError);
+ } else {
+ info->finish(Thing::ThingErrorHardwareFailure);
+ }
+ m_pluginTimer->timeout(); // get a status update
+ }
+}
+
+void IntegrationPluginBluOS::onVolumeReceived(int volume, bool mute)
+{
+ BluOS *bluos = static_cast(sender());
+ Thing *thing = myThings().findById(m_bluos.key(bluos));
+ if (!thing){
+ qCWarning(dcBluOS()) << "Could not find any Thing that belongs to this BluOS object";
+ return;
+ }
+ thing->setStateValue(bluosPlayerMuteStateTypeId, mute);
+ thing->setStateValue(bluosPlayerVolumeStateTypeId, volume);
+}
+
+void IntegrationPluginBluOS::onShuffleStateReceived(bool state)
+{
+ BluOS *bluos = static_cast(sender());
+ Thing *thing = myThings().findById(m_bluos.key(bluos));
+ if (!thing)
+ return;
+ thing->setStateValue(bluosPlayerShuffleStateTypeId, state);
+}
+
+void IntegrationPluginBluOS::onRepeatModeReceived(BluOS::RepeatMode mode)
+{
+ BluOS *bluos = static_cast(sender());
+ Thing *thing = myThings().findById(m_bluos.key(bluos));
+ if (!thing){
+ qCWarning(dcBluOS()) << "Could not find any Thing that belongs to this BluOS object";
+ return;
+ }
+ switch (mode) {
+ case BluOS::RepeatMode::All:
+ thing->setStateValue(bluosPlayerRepeatStateTypeId, "All");
+ break;
+ case BluOS::RepeatMode::One:
+ thing->setStateValue(bluosPlayerRepeatStateTypeId, "One");
+ break;
+ case BluOS::RepeatMode::None:
+ thing->setStateValue(bluosPlayerRepeatStateTypeId, "None");
+ break;
+ }
+
+}
+
+void IntegrationPluginBluOS::onPresetsReceived(QUuid requestId, const QList &presets)
+{
+ BluOS *bluos = static_cast(sender());
+ Thing *thing = myThings().findById(m_bluos.key(bluos));
+
+ if (m_asyncBrowseResults.contains(requestId)) {
+ BrowseResult *result = m_asyncBrowseResults.take(requestId);
+ if (!thing) {
+ qCWarning(dcBluOS()) << "Could not find any Thing that belongs to this browse result";
+ result->finish(Thing::ThingErrorHardwareNotAvailable);
+ return;
+ }
+ foreach(BluOS::Preset preset, presets) {
+ qCDebug(dcBluOS()) << "Preset added" << preset.Name << preset.Id << preset.Url;
+ BrowserItem item("presets&"+QString::number(preset.Id), preset.Name, false, true);
+ item.setIcon(BrowserItem::BrowserIcon::BrowserIconFavorites);
+ result->addItem(item);
+ }
+ result->finish(Thing::ThingErrorNoError);
+ }
+ if (m_asyncBrowseItemResults.contains(requestId)) {
+ BrowserItemResult *result = m_asyncBrowseItemResults.take(requestId);
+ result->finish(Thing::ThingErrorItemNotFound);
+ //For future browsing features
+ }
+}
+
+void IntegrationPluginBluOS::onSourcesReceived(QUuid requestId, const QList &sources)
+{
+ BluOS *bluos = static_cast(sender());
+ Thing *thing = myThings().findById(m_bluos.key(bluos));
+
+ if (m_asyncBrowseResults.contains(requestId)) {
+ BrowseResult *result = m_asyncBrowseResults.take(requestId);
+ if (!thing) {
+ qCWarning(dcBluOS()) << "Could not find any Thing that belongs to this browse result";
+ result->finish(Thing::ThingErrorHardwareNotAvailable);
+ return;
+ }
+ foreach(BluOS::Source source, sources) {
+ qCDebug(dcBluOS()) << "Source added" << source.Text << source.BrowseKey << source.Type;
+ MediaBrowserItem item;
+ item.setDisplayName(source.Text);
+ if (source.BrowseKey.isEmpty()) {
+ item.setBrowsable(false);
+ item.setExecutable(true);
+ item.setId(source.Text);
+ } else {
+ item.setBrowsable(true);
+ item.setExecutable(false);
+ item.setId(source.BrowseKey);
+ }
+ item.setIcon(BrowserItem::BrowserIconMusic);
+ if (source.Text == "Bluetooth") {
+ item.setMediaIcon(MediaBrowserItem::MediaBrowserIconBluetooth);
+ //result->addItem(item);
+ } else if (source.Text == "Spotify") {
+ item.setExecutable(false);
+ item.setBrowsable(false);
+ item.setMediaIcon(MediaBrowserItem::MediaBrowserIconSpotify);
+ item.setDescription("Open the Spotify App for browsing");
+ result->addItem(item);
+ } else if (source.Text == "TuneIn") {
+ item.setMediaIcon(MediaBrowserItem::MediaBrowserIconTuneIn);
+ result->addItem(item);
+ } else if (source.Text.contains("Aux")) {
+ item.setMediaIcon(MediaBrowserItem::MediaBrowserIconAux);
+ result->addItem(item);
+ } else if (source.Text == "Radio Paradise") {
+ //item.setMediaIcon(MediaBrowserItem::MediaBrowserIconRadioParadise);
+ //result->addItem(item);
+ //Needs testing before continuing
+ }
+ }
+ result->finish(Thing::ThingErrorNoError);
+ }
+
+ if (m_asyncBrowseItemResults.contains(requestId)) {
+ BrowserItemResult *result = m_asyncBrowseItemResults.take(requestId);
+ result->finish(Thing::ThingErrorItemNotFound);
+ //For future browsing features
+ }
+}
+
+void IntegrationPluginBluOS::onBrowseResultReceived(QUuid requestId, const QList &sources)
+{
+ BluOS *bluos = static_cast(sender());
+ Thing *thing = myThings().findById(m_bluos.key(bluos));
+
+ if (m_asyncBrowseResults.contains(requestId)) {
+ BrowseResult *result = m_asyncBrowseResults.take(requestId);
+
+ if (!thing) {
+ qCWarning(dcBluOS()) << "Could not find any Thing that belongs to this browse result";
+ result->finish(Thing::ThingErrorHardwareNotAvailable);
+ return;
+ }
+ foreach(BluOS::Source source, sources) {
+ qCDebug(dcBluOS()) << "Source added" << source.Text << source.BrowseKey << source.Type;
+ MediaBrowserItem item;
+ item.setDisplayName(source.Text);
+ if (source.BrowseKey.isEmpty()) {
+ item.setBrowsable(false);
+ item.setExecutable(true);
+ item.setId(source.Text);
+ } else {
+ item.setBrowsable(true);
+ item.setExecutable(false);
+ item.setId(source.BrowseKey);
+ }
+ item.setIcon(BrowserItem::BrowserIconMusic);
+ result->addItem(item);
+ }
+ result->finish(Thing::ThingErrorNoError);
+ }
+}
diff --git a/bluos/integrationpluginbluos.h b/bluos/integrationpluginbluos.h
new file mode 100644
index 00000000..2089ad1e
--- /dev/null
+++ b/bluos/integrationpluginbluos.h
@@ -0,0 +1,92 @@
+/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+*
+* Copyright 2013 - 2020, nymea GmbH
+* Contact: contact@nymea.io
+*
+* This file is part of nymea.
+* This project including source code and documentation is protected by
+* copyright law, and remains the property of nymea GmbH. All rights, including
+* reproduction, publication, editing and translation, are reserved. The use of
+* this project is subject to the terms of a license agreement to be concluded
+* with nymea GmbH in accordance with the terms of use of nymea GmbH, available
+* under https://nymea.io/license
+*
+* GNU Lesser General Public License Usage
+* Alternatively, this project may be redistributed and/or modified under the
+* terms of the GNU Lesser General Public License as published by the Free
+* Software Foundation; version 3. This project is distributed in the hope that
+* it will be useful, but WITHOUT ANY WARRANTY; without even the implied
+* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+* Lesser General Public License for more details.
+*
+* You should have received a copy of the GNU Lesser General Public License
+* along with this project. If not, see .
+*
+* For any further details and any questions please contact us under
+* contact@nymea.io or see our FAQ/Licensing Information on
+* https://nymea.io/license/faq
+*
+* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
+
+#ifndef INTEGRATIONPLUGINBLUOS_H
+#define INTEGRATIONPLUGINBLUOS_H
+
+#include "bluos.h"
+
+#include "integrations/integrationplugin.h"
+#include "platform/platformzeroconfcontroller.h"
+#include "network/zeroconf/zeroconfservicebrowser.h"
+#include "plugintimer.h"
+
+#include
+#include
+
+class PluginTimer;
+
+class IntegrationPluginBluOS: public IntegrationPlugin
+{
+ Q_OBJECT
+
+ Q_PLUGIN_METADATA(IID "io.nymea.IntegrationPlugin" FILE "integrationpluginbluos.json")
+ Q_INTERFACES(IntegrationPlugin)
+
+public:
+ explicit IntegrationPluginBluOS();
+
+ void init() override;
+ void discoverThings(ThingDiscoveryInfo *info) override;
+ void setupThing(ThingSetupInfo *info) override;
+ void postSetupThing(Thing *thing) override;
+ void thingRemoved(Thing *thing) override;
+ void executeAction(ThingActionInfo *info) override;
+
+ void browseThing(BrowseResult *result) override;
+ void browserItem(BrowserItemResult *result) override;
+ void executeBrowserItem(BrowserActionInfo *info) override;
+
+private:
+ PluginTimer *m_pluginTimer = nullptr;
+ ZeroConfServiceBrowser *m_serviceBrowser = nullptr;
+
+ QHash m_bluos;
+ QHash m_asyncSetup;
+ QHash m_asyncActions;
+ QHash m_asyncBrowseResults;
+ QHash m_asyncExecuteBrowseItems;
+ QHash m_asyncBrowseItemResults;
+
+private slots:
+ void onPluginTimer();
+
+ void onConnectionChanged(bool connected);
+ void onStatusResponseReceived(const BluOS::StatusResponse &status);
+ void onActionExecuted(QUuid actionId, bool success);
+ void onVolumeReceived(int volume, bool mute);
+ void onShuffleStateReceived(bool state);
+ void onRepeatModeReceived(BluOS::RepeatMode mode);
+
+ void onPresetsReceived(QUuid requestId, const QList &presets);
+ void onSourcesReceived(QUuid requestId, const QList &sources);
+ void onBrowseResultReceived(QUuid requestId, const QList &sources);
+};
+#endif // INTEGRATIONPLUGINBLUOS_H
diff --git a/bluos/integrationpluginbluos.json b/bluos/integrationpluginbluos.json
new file mode 100644
index 00000000..f9debc8f
--- /dev/null
+++ b/bluos/integrationpluginbluos.json
@@ -0,0 +1,198 @@
+{
+ "displayName": "BluOS",
+ "name": "BluOS",
+ "id": "71dd25b3-37ef-4b27-abca-24989fa38c61",
+ "vendors": [
+ {
+ "id": "39a492e9-e497-4b43-94d4-970eb9913b96",
+ "displayName": "BluOS",
+ "name": "bluos",
+ "thingClasses": [
+ {
+ "id": "406adcbc-1e7d-4e41-ae2a-f87b6bafd13d",
+ "name": "bluosPlayer",
+ "displayName": "BluOS player",
+ "createMethods": ["discovery"],
+ "interfaces": ["mediaplayer", "extendedvolumecontroller", "mediametadataprovider", "shufflerepeat", "connectable"],
+ "browsable": true,
+ "paramTypes":[
+ {
+ "id": "833f99cc-fc3f-48ef-a705-a69ae2c8e9ec",
+ "name": "address",
+ "displayName": "IP Address",
+ "type" : "QString"
+ },
+ {
+ "id": "4628c040-6bbb-43c9-b25f-ce3b22300e3b",
+ "name": "port",
+ "displayName": "Port",
+ "type" : "int"
+ },
+ {
+ "id": "8fd2a7d5-bb26-4f18-a488-5ab0779733f8",
+ "name": "serialNumber",
+ "displayName": "Serial number",
+ "type" : "QString"
+ }
+ ],
+ "stateTypes": [
+ {
+ "id": "8092f387-5099-43e8-a7c4-7f0dd7b70fce",
+ "name": "connected",
+ "displayName": "Connected",
+ "displayNameEvent": "Connected changed",
+ "type": "bool",
+ "defaultValue": false,
+ "cached": false
+ },
+ {
+ "id": "2f650ae6-f3a5-4851-8449-1ba02c4864e5",
+ "name": "mute",
+ "displayName": "Mute",
+ "displayNameEvent": "Mute changed",
+ "displayNameAction": "Set mute",
+ "type": "bool",
+ "defaultValue": false,
+ "cached": false,
+ "writable": true
+ },
+ {
+ "id": "a444bb7c-7266-4c7e-874a-eb04bb91b9cf",
+ "name": "volume",
+ "displayName": "Volume",
+ "displayNameEvent": "Volume changed",
+ "displayNameAction": "Set volume",
+ "type": "int",
+ "defaultValue": 50,
+ "minValue": 0,
+ "maxValue": 100,
+ "writable": true
+ },
+ {
+ "id": "43f552c7-7dfe-4358-bebd-ab558191cdfc",
+ "name": "playbackStatus",
+ "displayName": "Playback status",
+ "displayNameEvent": "Playback status changed",
+ "displayNameAction": "Set playback status",
+ "type": "QString",
+ "defaultValue": "Stopped",
+ "possibleValues": ["Playing", "Paused", "Stopped"],
+ "cached": false,
+ "writable": true
+ },
+ {
+ "id": "5636c21a-f14b-472c-8486-177543f6adfb",
+ "name": "shuffle",
+ "displayName": "Shuffle",
+ "displayNameEvent": "Shuffle changed",
+ "displayNameAction": "Set shuffle",
+ "type": "bool",
+ "defaultValue": false,
+ "cached": false,
+ "writable": true
+ },
+ {
+ "id": "b4a572b1-6120-43e8-9a6c-18d099c8b162",
+ "name": "repeat",
+ "displayName": "Repeat mode",
+ "displayNameEvent": "Repeat mode changed",
+ "displayNameAction": "Set repeat mode",
+ "type": "QString",
+ "defaultValue": "None",
+ "possibleValues": ["None", "One", "All"],
+ "cached": false,
+ "writable": true
+ },
+ {
+ "id": "604e4995-ee1a-44fc-bfcb-ca0e861710bd",
+ "name": "source",
+ "displayName": "Source",
+ "displayNameEvent": "Source changed",
+ "type": "QString",
+ "defaultValue": ""
+ },
+ {
+ "id": "8c5372f1-3dde-4984-9955-9a436a29e8e3",
+ "name": "artist",
+ "displayName": "Artist",
+ "displayNameEvent": "Artist changed",
+ "type": "QString",
+ "defaultValue": ""
+ },
+ {
+ "id": "d2a2db24-5855-40cd-a043-6f67e43acc61",
+ "name": "collection",
+ "displayName": "Album",
+ "displayNameEvent": "Album changed",
+ "type": "QString",
+ "defaultValue": ""
+ },
+ {
+ "id": "6204ec9d-6aab-4fd3-8911-fdaa462c19a8",
+ "name": "title",
+ "displayName": "Title",
+ "displayNameEvent": "Title changed",
+ "type": "QString",
+ "defaultValue": ""
+ },
+ {
+ "id": "918bfeed-bf96-4034-85d6-9f4cff35fd00",
+ "name": "artwork",
+ "displayName": "Artwork",
+ "displayNameEvent": "Artwork changed",
+ "type": "QString",
+ "defaultValue": ""
+ },
+ {
+ "id": "69757ef3-173f-499c-9304-4252de3588c6",
+ "name": "group",
+ "displayName": "Group",
+ "displayNameEvent": "Group changed",
+ "type": "QString",
+ "defaultValue": ""
+ },
+ {
+ "id": "e5222e93-fa14-49de-950f-2f605ce8927b",
+ "name": "playerType",
+ "displayName": "Player type",
+ "displayNameEvent": "Player type changed",
+ "possibleValues": [
+ "audio",
+ "video"
+ ],
+ "type": "QString",
+ "defaultValue": "audio"
+ }
+ ],
+ "actionTypes": [
+ {
+ "id": "864a5bcc-71e1-4d7f-8f2e-3c4222d5d988",
+ "name": "skipBack",
+ "displayName": "Skip back"
+ },
+ {
+ "id": "2526ec6d-1c21-4f73-97f1-973a5f05b626",
+ "name": "stop",
+ "displayName": "Stop"
+ },
+ {
+ "id": "253eb62f-d50d-4667-8213-8632de178aa3",
+ "name": "play",
+ "displayName": "Play"
+ },
+ {
+ "id": "92446566-8c32-4ee2-9498-d9dd9333a75d",
+ "name": "pause",
+ "displayName": "Pause"
+ },
+ {
+ "id": "30930095-6f97-48ef-8bbf-597d734f0751",
+ "name": "skipNext",
+ "displayName": "Skip next"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+}
diff --git a/bluos/meta.json b/bluos/meta.json
new file mode 100644
index 00000000..862cdd2a
--- /dev/null
+++ b/bluos/meta.json
@@ -0,0 +1,13 @@
+{
+ "title": "BluOS",
+ "tagline": "Control audio devices based on BluOS.",
+ "icon": "blusound.png",
+ "stability": "consumer",
+ "offline": true,
+ "technologies": [
+ "network"
+ ],
+ "categories": [
+ "multimedia"
+ ]
+}
diff --git a/bluos/translations/71dd25b3-37ef-4b27-abca-24989fa38c61-de.ts b/bluos/translations/71dd25b3-37ef-4b27-abca-24989fa38c61-de.ts
new file mode 100644
index 00000000..7ee43f88
--- /dev/null
+++ b/bluos/translations/71dd25b3-37ef-4b27-abca-24989fa38c61-de.ts
@@ -0,0 +1,295 @@
+
+
+
+
+ BluOS
+
+
+
+ Album
+ The name of the ParamType (ThingClass: bluosPlayer, EventType: collection, ID: {d2a2db24-5855-40cd-a043-6f67e43acc61})
+----------
+The name of the StateType ({d2a2db24-5855-40cd-a043-6f67e43acc61}) of ThingClass bluosPlayer
+ Album
+
+
+
+ Album changed
+ The name of the EventType ({d2a2db24-5855-40cd-a043-6f67e43acc61}) of ThingClass bluosPlayer
+ Album geändert
+
+
+
+
+ Artist
+ The name of the ParamType (ThingClass: bluosPlayer, EventType: artist, ID: {8c5372f1-3dde-4984-9955-9a436a29e8e3})
+----------
+The name of the StateType ({8c5372f1-3dde-4984-9955-9a436a29e8e3}) of ThingClass bluosPlayer
+ Künstler
+
+
+
+ Artist changed
+ The name of the EventType ({8c5372f1-3dde-4984-9955-9a436a29e8e3}) of ThingClass bluosPlayer
+ Künstler geändert
+
+
+
+
+ Artwork
+ The name of the ParamType (ThingClass: bluosPlayer, EventType: artwork, ID: {918bfeed-bf96-4034-85d6-9f4cff35fd00})
+----------
+The name of the StateType ({918bfeed-bf96-4034-85d6-9f4cff35fd00}) of ThingClass bluosPlayer
+ Artwork
+
+
+
+ Artwork changed
+ The name of the EventType ({918bfeed-bf96-4034-85d6-9f4cff35fd00}) of ThingClass bluosPlayer
+ Artwork geändert
+
+
+
+
+ BluOS
+ The name of the vendor ({39a492e9-e497-4b43-94d4-970eb9913b96})
+----------
+The name of the plugin BluOS ({71dd25b3-37ef-4b27-abca-24989fa38c61})
+ BluOS
+
+
+
+ BluOS player
+ The name of the ThingClass ({406adcbc-1e7d-4e41-ae2a-f87b6bafd13d})
+ BluOS Player
+
+
+
+
+ Connected
+ The name of the ParamType (ThingClass: bluosPlayer, EventType: connected, ID: {8092f387-5099-43e8-a7c4-7f0dd7b70fce})
+----------
+The name of the StateType ({8092f387-5099-43e8-a7c4-7f0dd7b70fce}) of ThingClass bluosPlayer
+ Verbunden
+
+
+
+ Connected changed
+ The name of the EventType ({8092f387-5099-43e8-a7c4-7f0dd7b70fce}) of ThingClass bluosPlayer
+ Verbunden geändert
+
+
+
+
+ Group
+ The name of the ParamType (ThingClass: bluosPlayer, EventType: group, ID: {69757ef3-173f-499c-9304-4252de3588c6})
+----------
+The name of the StateType ({69757ef3-173f-499c-9304-4252de3588c6}) of ThingClass bluosPlayer
+ Gruppe
+
+
+
+ Group changed
+ The name of the EventType ({69757ef3-173f-499c-9304-4252de3588c6}) of ThingClass bluosPlayer
+ Gruppe geändert
+
+
+
+ IP Address
+ The name of the ParamType (ThingClass: bluosPlayer, Type: thing, ID: {833f99cc-fc3f-48ef-a705-a69ae2c8e9ec})
+ IP-Adresse
+
+
+
+
+
+ Mute
+ The name of the ParamType (ThingClass: bluosPlayer, ActionType: mute, ID: {2f650ae6-f3a5-4851-8449-1ba02c4864e5})
+----------
+The name of the ParamType (ThingClass: bluosPlayer, EventType: mute, ID: {2f650ae6-f3a5-4851-8449-1ba02c4864e5})
+----------
+The name of the StateType ({2f650ae6-f3a5-4851-8449-1ba02c4864e5}) of ThingClass bluosPlayer
+ Stumm
+
+
+
+ Mute changed
+ The name of the EventType ({2f650ae6-f3a5-4851-8449-1ba02c4864e5}) of ThingClass bluosPlayer
+ Stumm geändert
+
+
+
+ Pause
+ The name of the ActionType ({92446566-8c32-4ee2-9498-d9dd9333a75d}) of ThingClass bluosPlayer
+ Pause
+
+
+
+ Play
+ The name of the ActionType ({253eb62f-d50d-4667-8213-8632de178aa3}) of ThingClass bluosPlayer
+ Play
+
+
+
+
+
+ Playback status
+ The name of the ParamType (ThingClass: bluosPlayer, ActionType: playbackStatus, ID: {43f552c7-7dfe-4358-bebd-ab558191cdfc})
+----------
+The name of the ParamType (ThingClass: bluosPlayer, EventType: playbackStatus, ID: {43f552c7-7dfe-4358-bebd-ab558191cdfc})
+----------
+The name of the StateType ({43f552c7-7dfe-4358-bebd-ab558191cdfc}) of ThingClass bluosPlayer
+ Playbackstatus
+
+
+
+ Playback status changed
+ The name of the EventType ({43f552c7-7dfe-4358-bebd-ab558191cdfc}) of ThingClass bluosPlayer
+ Playbackstatus geändert
+
+
+
+ Port
+ The name of the ParamType (ThingClass: bluosPlayer, Type: thing, ID: {4628c040-6bbb-43c9-b25f-ce3b22300e3b})
+ Port
+
+
+
+
+
+ Repeat mode
+ The name of the ParamType (ThingClass: bluosPlayer, ActionType: repeat, ID: {b4a572b1-6120-43e8-9a6c-18d099c8b162})
+----------
+The name of the ParamType (ThingClass: bluosPlayer, EventType: repeat, ID: {b4a572b1-6120-43e8-9a6c-18d099c8b162})
+----------
+The name of the StateType ({b4a572b1-6120-43e8-9a6c-18d099c8b162}) of ThingClass bluosPlayer
+ Wiederholungsmodus
+
+
+
+ Repeat mode changed
+ The name of the EventType ({b4a572b1-6120-43e8-9a6c-18d099c8b162}) of ThingClass bluosPlayer
+ Wiederholungsmodus geändert
+
+
+
+ Serial number
+ The name of the ParamType (ThingClass: bluosPlayer, Type: thing, ID: {8fd2a7d5-bb26-4f18-a488-5ab0779733f8})
+ Seriennummer
+
+
+
+ Set mute
+ The name of the ActionType ({2f650ae6-f3a5-4851-8449-1ba02c4864e5}) of ThingClass bluosPlayer
+ Setze stumm
+
+
+
+ Set playback status
+ The name of the ActionType ({43f552c7-7dfe-4358-bebd-ab558191cdfc}) of ThingClass bluosPlayer
+ Setze Playbackstatus
+
+
+
+ Set repeat mode
+ The name of the ActionType ({b4a572b1-6120-43e8-9a6c-18d099c8b162}) of ThingClass bluosPlayer
+ Setze Wiederholungsmodus
+
+
+
+ Set shuffle
+ The name of the ActionType ({5636c21a-f14b-472c-8486-177543f6adfb}) of ThingClass bluosPlayer
+ Setze Zufallswiedergabe
+
+
+
+ Set volume
+ The name of the ActionType ({a444bb7c-7266-4c7e-874a-eb04bb91b9cf}) of ThingClass bluosPlayer
+ Setze Lautstärke
+
+
+
+
+
+ Shuffle
+ The name of the ParamType (ThingClass: bluosPlayer, ActionType: shuffle, ID: {5636c21a-f14b-472c-8486-177543f6adfb})
+----------
+The name of the ParamType (ThingClass: bluosPlayer, EventType: shuffle, ID: {5636c21a-f14b-472c-8486-177543f6adfb})
+----------
+The name of the StateType ({5636c21a-f14b-472c-8486-177543f6adfb}) of ThingClass bluosPlayer
+ Zufallswiedergabe
+
+
+
+ Shuffle changed
+ The name of the EventType ({5636c21a-f14b-472c-8486-177543f6adfb}) of ThingClass bluosPlayer
+ Zufallswiedergabe geändert
+
+
+
+ Skip back
+ The name of the ActionType ({864a5bcc-71e1-4d7f-8f2e-3c4222d5d988}) of ThingClass bluosPlayer
+ Vorheriges
+
+
+
+ Skip next
+ The name of the ActionType ({30930095-6f97-48ef-8bbf-597d734f0751}) of ThingClass bluosPlayer
+ Nächstes
+
+
+
+
+ Source
+ The name of the ParamType (ThingClass: bluosPlayer, EventType: source, ID: {604e4995-ee1a-44fc-bfcb-ca0e861710bd})
+----------
+The name of the StateType ({604e4995-ee1a-44fc-bfcb-ca0e861710bd}) of ThingClass bluosPlayer
+ Quelle
+
+
+
+ Source changed
+ The name of the EventType ({604e4995-ee1a-44fc-bfcb-ca0e861710bd}) of ThingClass bluosPlayer
+ Quelle geändert
+
+
+
+ Stop
+ The name of the ActionType ({2526ec6d-1c21-4f73-97f1-973a5f05b626}) of ThingClass bluosPlayer
+ Stopp
+
+
+
+
+ Title
+ The name of the ParamType (ThingClass: bluosPlayer, EventType: title, ID: {6204ec9d-6aab-4fd3-8911-fdaa462c19a8})
+----------
+The name of the StateType ({6204ec9d-6aab-4fd3-8911-fdaa462c19a8}) of ThingClass bluosPlayer
+ Titel
+
+
+
+ Title changed
+ The name of the EventType ({6204ec9d-6aab-4fd3-8911-fdaa462c19a8}) of ThingClass bluosPlayer
+ Titel geändert
+
+
+
+
+
+ Volume
+ The name of the ParamType (ThingClass: bluosPlayer, ActionType: volume, ID: {a444bb7c-7266-4c7e-874a-eb04bb91b9cf})
+----------
+The name of the ParamType (ThingClass: bluosPlayer, EventType: volume, ID: {a444bb7c-7266-4c7e-874a-eb04bb91b9cf})
+----------
+The name of the StateType ({a444bb7c-7266-4c7e-874a-eb04bb91b9cf}) of ThingClass bluosPlayer
+ Lautstärke
+
+
+
+ Volume changed
+ The name of the EventType ({a444bb7c-7266-4c7e-874a-eb04bb91b9cf}) of ThingClass bluosPlayer
+ Lautstärke geändert
+
+
+
diff --git a/bluos/translations/71dd25b3-37ef-4b27-abca-24989fa38c61-en_US.ts b/bluos/translations/71dd25b3-37ef-4b27-abca-24989fa38c61-en_US.ts
new file mode 100644
index 00000000..2f97198a
--- /dev/null
+++ b/bluos/translations/71dd25b3-37ef-4b27-abca-24989fa38c61-en_US.ts
@@ -0,0 +1,295 @@
+
+
+
+
+ BluOS
+
+
+
+ Album
+ The name of the ParamType (ThingClass: bluosPlayer, EventType: collection, ID: {d2a2db24-5855-40cd-a043-6f67e43acc61})
+----------
+The name of the StateType ({d2a2db24-5855-40cd-a043-6f67e43acc61}) of ThingClass bluosPlayer
+
+
+
+
+ Album changed
+ The name of the EventType ({d2a2db24-5855-40cd-a043-6f67e43acc61}) of ThingClass bluosPlayer
+
+
+
+
+
+ Artist
+ The name of the ParamType (ThingClass: bluosPlayer, EventType: artist, ID: {8c5372f1-3dde-4984-9955-9a436a29e8e3})
+----------
+The name of the StateType ({8c5372f1-3dde-4984-9955-9a436a29e8e3}) of ThingClass bluosPlayer
+
+
+
+
+ Artist changed
+ The name of the EventType ({8c5372f1-3dde-4984-9955-9a436a29e8e3}) of ThingClass bluosPlayer
+
+
+
+
+
+ Artwork
+ The name of the ParamType (ThingClass: bluosPlayer, EventType: artwork, ID: {918bfeed-bf96-4034-85d6-9f4cff35fd00})
+----------
+The name of the StateType ({918bfeed-bf96-4034-85d6-9f4cff35fd00}) of ThingClass bluosPlayer
+
+
+
+
+ Artwork changed
+ The name of the EventType ({918bfeed-bf96-4034-85d6-9f4cff35fd00}) of ThingClass bluosPlayer
+
+
+
+
+
+ BluOS
+ The name of the vendor ({39a492e9-e497-4b43-94d4-970eb9913b96})
+----------
+The name of the plugin BluOS ({71dd25b3-37ef-4b27-abca-24989fa38c61})
+
+
+
+
+ BluOS player
+ The name of the ThingClass ({406adcbc-1e7d-4e41-ae2a-f87b6bafd13d})
+
+
+
+
+
+ Connected
+ The name of the ParamType (ThingClass: bluosPlayer, EventType: connected, ID: {8092f387-5099-43e8-a7c4-7f0dd7b70fce})
+----------
+The name of the StateType ({8092f387-5099-43e8-a7c4-7f0dd7b70fce}) of ThingClass bluosPlayer
+
+
+
+
+ Connected changed
+ The name of the EventType ({8092f387-5099-43e8-a7c4-7f0dd7b70fce}) of ThingClass bluosPlayer
+
+
+
+
+
+ Group
+ The name of the ParamType (ThingClass: bluosPlayer, EventType: group, ID: {69757ef3-173f-499c-9304-4252de3588c6})
+----------
+The name of the StateType ({69757ef3-173f-499c-9304-4252de3588c6}) of ThingClass bluosPlayer
+
+
+
+
+ Group changed
+ The name of the EventType ({69757ef3-173f-499c-9304-4252de3588c6}) of ThingClass bluosPlayer
+
+
+
+
+ IP Address
+ The name of the ParamType (ThingClass: bluosPlayer, Type: thing, ID: {833f99cc-fc3f-48ef-a705-a69ae2c8e9ec})
+
+
+
+
+
+
+ Mute
+ The name of the ParamType (ThingClass: bluosPlayer, ActionType: mute, ID: {2f650ae6-f3a5-4851-8449-1ba02c4864e5})
+----------
+The name of the ParamType (ThingClass: bluosPlayer, EventType: mute, ID: {2f650ae6-f3a5-4851-8449-1ba02c4864e5})
+----------
+The name of the StateType ({2f650ae6-f3a5-4851-8449-1ba02c4864e5}) of ThingClass bluosPlayer
+
+
+
+
+ Mute changed
+ The name of the EventType ({2f650ae6-f3a5-4851-8449-1ba02c4864e5}) of ThingClass bluosPlayer
+
+
+
+
+ Pause
+ The name of the ActionType ({92446566-8c32-4ee2-9498-d9dd9333a75d}) of ThingClass bluosPlayer
+
+
+
+
+ Play
+ The name of the ActionType ({253eb62f-d50d-4667-8213-8632de178aa3}) of ThingClass bluosPlayer
+
+
+
+
+
+
+ Playback status
+ The name of the ParamType (ThingClass: bluosPlayer, ActionType: playbackStatus, ID: {43f552c7-7dfe-4358-bebd-ab558191cdfc})
+----------
+The name of the ParamType (ThingClass: bluosPlayer, EventType: playbackStatus, ID: {43f552c7-7dfe-4358-bebd-ab558191cdfc})
+----------
+The name of the StateType ({43f552c7-7dfe-4358-bebd-ab558191cdfc}) of ThingClass bluosPlayer
+
+
+
+
+ Playback status changed
+ The name of the EventType ({43f552c7-7dfe-4358-bebd-ab558191cdfc}) of ThingClass bluosPlayer
+
+
+
+
+ Port
+ The name of the ParamType (ThingClass: bluosPlayer, Type: thing, ID: {4628c040-6bbb-43c9-b25f-ce3b22300e3b})
+
+
+
+
+
+
+ Repeat mode
+ The name of the ParamType (ThingClass: bluosPlayer, ActionType: repeat, ID: {b4a572b1-6120-43e8-9a6c-18d099c8b162})
+----------
+The name of the ParamType (ThingClass: bluosPlayer, EventType: repeat, ID: {b4a572b1-6120-43e8-9a6c-18d099c8b162})
+----------
+The name of the StateType ({b4a572b1-6120-43e8-9a6c-18d099c8b162}) of ThingClass bluosPlayer
+
+
+
+
+ Repeat mode changed
+ The name of the EventType ({b4a572b1-6120-43e8-9a6c-18d099c8b162}) of ThingClass bluosPlayer
+
+
+
+
+ Serial number
+ The name of the ParamType (ThingClass: bluosPlayer, Type: thing, ID: {8fd2a7d5-bb26-4f18-a488-5ab0779733f8})
+
+
+
+
+ Set mute
+ The name of the ActionType ({2f650ae6-f3a5-4851-8449-1ba02c4864e5}) of ThingClass bluosPlayer
+
+
+
+
+ Set playback status
+ The name of the ActionType ({43f552c7-7dfe-4358-bebd-ab558191cdfc}) of ThingClass bluosPlayer
+
+
+
+
+ Set repeat mode
+ The name of the ActionType ({b4a572b1-6120-43e8-9a6c-18d099c8b162}) of ThingClass bluosPlayer
+
+
+
+
+ Set shuffle
+ The name of the ActionType ({5636c21a-f14b-472c-8486-177543f6adfb}) of ThingClass bluosPlayer
+
+
+
+
+ Set volume
+ The name of the ActionType ({a444bb7c-7266-4c7e-874a-eb04bb91b9cf}) of ThingClass bluosPlayer
+
+
+
+
+
+
+ Shuffle
+ The name of the ParamType (ThingClass: bluosPlayer, ActionType: shuffle, ID: {5636c21a-f14b-472c-8486-177543f6adfb})
+----------
+The name of the ParamType (ThingClass: bluosPlayer, EventType: shuffle, ID: {5636c21a-f14b-472c-8486-177543f6adfb})
+----------
+The name of the StateType ({5636c21a-f14b-472c-8486-177543f6adfb}) of ThingClass bluosPlayer
+
+
+
+
+ Shuffle changed
+ The name of the EventType ({5636c21a-f14b-472c-8486-177543f6adfb}) of ThingClass bluosPlayer
+
+
+
+
+ Skip back
+ The name of the ActionType ({864a5bcc-71e1-4d7f-8f2e-3c4222d5d988}) of ThingClass bluosPlayer
+
+
+
+
+ Skip next
+ The name of the ActionType ({30930095-6f97-48ef-8bbf-597d734f0751}) of ThingClass bluosPlayer
+
+
+
+
+
+ Source
+ The name of the ParamType (ThingClass: bluosPlayer, EventType: source, ID: {604e4995-ee1a-44fc-bfcb-ca0e861710bd})
+----------
+The name of the StateType ({604e4995-ee1a-44fc-bfcb-ca0e861710bd}) of ThingClass bluosPlayer
+
+
+
+
+ Source changed
+ The name of the EventType ({604e4995-ee1a-44fc-bfcb-ca0e861710bd}) of ThingClass bluosPlayer
+
+
+
+
+ Stop
+ The name of the ActionType ({2526ec6d-1c21-4f73-97f1-973a5f05b626}) of ThingClass bluosPlayer
+
+
+
+
+
+ Title
+ The name of the ParamType (ThingClass: bluosPlayer, EventType: title, ID: {6204ec9d-6aab-4fd3-8911-fdaa462c19a8})
+----------
+The name of the StateType ({6204ec9d-6aab-4fd3-8911-fdaa462c19a8}) of ThingClass bluosPlayer
+
+
+
+
+ Title changed
+ The name of the EventType ({6204ec9d-6aab-4fd3-8911-fdaa462c19a8}) of ThingClass bluosPlayer
+
+
+
+
+
+
+ Volume
+ The name of the ParamType (ThingClass: bluosPlayer, ActionType: volume, ID: {a444bb7c-7266-4c7e-874a-eb04bb91b9cf})
+----------
+The name of the ParamType (ThingClass: bluosPlayer, EventType: volume, ID: {a444bb7c-7266-4c7e-874a-eb04bb91b9cf})
+----------
+The name of the StateType ({a444bb7c-7266-4c7e-874a-eb04bb91b9cf}) of ThingClass bluosPlayer
+
+
+
+
+ Volume changed
+ The name of the EventType ({a444bb7c-7266-4c7e-874a-eb04bb91b9cf}) of ThingClass bluosPlayer
+
+
+
+
diff --git a/debian/control b/debian/control
index 99bed371..540a10b1 100644
--- a/debian/control
+++ b/debian/control
@@ -88,6 +88,21 @@ Description: nymea.io plugin for awattar
This package will install the nymea.io plugin for awattar
+Package: nymea-plugin-bluos
+Architecture: any
+Depends: ${shlibs:Depends},
+ ${misc:Depends},
+ nymea-plugins-translations,
+Description: nymea.io plugin for bluos
+ 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 bluos
+
+
Package: nymea-plugin-boblight
Architecture: any
Depends: ${shlibs:Depends},
diff --git a/debian/nymea-plugin-bluos.install.in b/debian/nymea-plugin-bluos.install.in
new file mode 100644
index 00000000..f0a17ed6
--- /dev/null
+++ b/debian/nymea-plugin-bluos.install.in
@@ -0,0 +1 @@
+usr/lib/@DEB_HOST_MULTIARCH@/nymea/plugins/libnymea_integrationpluginbluos.so
diff --git a/nymea-plugins.pro b/nymea-plugins.pro
index 08b0c5a9..6f139a71 100644
--- a/nymea-plugins.pro
+++ b/nymea-plugins.pro
@@ -5,6 +5,7 @@ PLUGIN_DIRS = \
aqi \
avahimonitor \
awattar \
+ bluos \
boblight \
bose \
coinmarketcap \