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 \