diff --git a/bluos/bluos.cpp b/bluos/bluos.cpp
index e69de29b..bda017eb 100644
--- a/bluos/bluos.cpp
+++ b/bluos/bluos.cpp
@@ -0,0 +1,481 @@
+/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+*
+* 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;
+}
+
+QUuid BluOS::getStatus()
+{
+ QUuid requestId = QUuid::createUuid();
+ 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, this, [reply, this] {
+ reply->deleteLater();
+ 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);
+ QXmlStreamReader xml;
+ xml.addData(reply->readAll());
+ if (xml.hasError()) {
+ qCDebug(dcBluOS()) << "XML Error:" << xml.errorString();
+ }
+
+ StatusResponse statusResponse;
+ if (xml.readNextStartElement()) {
+ if (xml.name() == "status") {
+ while(xml.readNextStartElement()){
+ if(xml.name() == "artist"){
+ } else 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"){
+ statusResponse.PlaybackState = xml.readElementText().toInt();
+ } else if(xml.name() == "volume"){
+ statusResponse.Volume = xml.readElementText().toInt();
+ } else if(xml.name() == "mute"){
+ statusResponse.Mute = xml.readElementText().toInt();
+ } else {
+ xml.skipCurrentElement();
+ }
+ }
+ }
+ }
+ emit statusReceived(statusResponse);
+ });
+ return requestId;
+}
+
+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, this, [requestId, reply, this] {
+ reply->deleteLater();
+ 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, this, [requestId, reply, this] {
+ reply->deleteLater();
+ 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, this, [requestId, reply, this] {
+ reply->deleteLater();
+ 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::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, this, [requestId, reply, this] {
+ reply->deleteLater();
+ 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::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, this, [reply, this] {
+ reply->deleteLater();
+ 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::loadPreset(int preset)
+{
+ Q_UNUSED(preset)
+ QUuid requestId = QUuid::createUuid();
+
+ QUrl url;
+ url.setScheme("http");
+ url.setHost(m_hostAddress.toString());
+ url.setPort(m_port);
+ url.setPath("/Presets");
+ QUrlQuery query;
+ query.addQueryItem("id", QString::number(preset));
+ url.setQuery(query);
+ QNetworkReply *reply = m_networkManager->get(QNetworkRequest(url));
+ connect(reply, &QNetworkReply::finished, this, [reply, this] {
+ reply->deleteLater();
+ 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::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, this, [reply, this] {
+ reply->deleteLater();
+ 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, this, [reply, this] {
+ reply->deleteLater();
+ 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, this, [requestId, reply, this] {
+ reply->deleteLater();
+ 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();
+ }
+ emit actionExecuted(requestId, true);
+ });
+ return requestId;
+}
diff --git a/bluos/bluos.h b/bluos/bluos.h
index e69de29b..59631aca 100644
--- a/bluos/bluos.h
+++ b/bluos/bluos.h
@@ -0,0 +1,127 @@
+/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+*
+* 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
+ };
+
+ struct StatusResponse {
+ QString Album;
+ QString Artist;
+ QString Name;
+ QString Service;
+ QUrl ServiceIcon;
+ QString PlaybackState;
+ QUrl StationUrl;
+ int Volume;
+ bool Mute;
+ RepeatMode Repeat;
+ bool Shuffle;
+ };
+
+ struct Preset {
+ int Prid;
+ QString name;
+ int Id;
+ QString url;
+ };
+
+ explicit BluOS(NetworkAccessManager *networkManager, QHostAddress hostAddress, int port, QObject *parent = nullptr);
+ int port();
+ QHostAddress hostAddress();
+
+ // Status Queries
+ QUuid 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
+
+ // 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);
+
+signals:
+ void connectionChanged(bool connected);
+ void actionExecuted(QUuid actionId, bool success);
+
+ void presetsReceived(const QList &presets);
+ void statusReceived(const StatusResponse &status);
+ void volumeReceived(int volume, bool mute);
+};
+#endif // BLUOS_H
diff --git a/bluos/integrationpluginbluos.cpp b/bluos/integrationpluginbluos.cpp
index 958b3017..f7a0efe3 100644
--- a/bluos/integrationpluginbluos.cpp
+++ b/bluos/integrationpluginbluos.cpp
@@ -33,8 +33,7 @@
#include "plugininfo.h"
#include "integrations/thing.h"
#include "network/networkaccessmanager.h"
-#include "platform/platformzeroconfcontroller.h"
-#include "network/zeroconf/zeroconfservicebrowser.h"
+
#include
#include
@@ -54,119 +53,242 @@ 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;
- if (info->thingClassId() == bluosThingClassId) {
- /*
- * The HEOS products can be discovered using the UPnP SSDP protocol. Through discovery,
- * the IP address of the HEOS products can be retrieved. Once the IP address is retrieved,
- * a telnet connection to port 1255 can be opened to access the HEOS CLI and control the HEOS system.
- * The HEOS product IP address can also be set statically and manually programmed into the control system.
- * Search target name (ST) in M-SEARCH discovery request is 'urn:schemas-denon-com:thing:ACT-Denon:1'.
- */
- UpnpDiscoveryReply *reply = hardwareManager()->upnpDiscovery()->discoverDevices();
- connect(reply, &UpnpDiscoveryReply::finished, info, [this, reply, info](){
- reply->deleteLater();
+ QString playerId = avahiEntry.hostName().split(".").first();
+ ThingDescriptor descriptor(bluosPlayerThingClassId, avahiEntry.name(), avahiEntry.hostAddress().toString());
+ ParamList params;
- if (reply->error() != UpnpDiscoveryReply::UpnpDiscoveryReplyErrorNoError) {
- qCWarning(dcDenon()) << "Upnp discovery error" << reply->error();
- info->finish(Thing::ThingErrorHardwareFailure, QT_TR_NOOP("UPnP discovery failed."));
- return;
- }
-
- foreach (const UpnpDeviceDescriptor &upnpDevice, reply->deviceDescriptors()) {
- qCDebug(dcDenon) << "UPnP thing found:" << upnpDevice.modelDescription() << upnpDevice.friendlyName() << upnpDevice.hostAddress().toString() << upnpDevice.modelName() << upnpDevice.manufacturer() << upnpDevice.serialNumber();
-
- if (upnpDevice.modelName().contains("HEOS")) {
- QString serialNumber = upnpDevice.serialNumber();
- if (serialNumber != "0000001") {
- // child devices have serial number 0000001
- qCDebug(dcDenon) << "UPnP thing found:" << upnpDevice.modelDescription() << upnpDevice.friendlyName() << upnpDevice.hostAddress().toString() << upnpDevice.modelName() << upnpDevice.manufacturer() << upnpDevice.serialNumber();
- ThingDescriptor descriptor(heosThingClassId, upnpDevice.modelName(), serialNumber);
- ParamList params;
- foreach (Thing *existingThing, myThings()) {
- if (existingThing->paramValue(heosThingSerialNumberParamTypeId).toString().contains(serialNumber, Qt::CaseSensitivity::CaseInsensitive)) {
- descriptor.setThingId(existingThing->id());
- break;
- }
- }
- params.append(Param(heosThingModelNameParamTypeId, upnpDevice.modelName()));
- params.append(Param(heosThingIpParamTypeId, upnpDevice.hostAddress().toString()));
- params.append(Param(heosThingSerialNumberParamTypeId, serialNumber));
- descriptor.setParams(params);
- info->addThingDescriptor(descriptor);
- }
+ foreach (Thing *existingDevice, myThings().filterByThingClassId(bluosPlayerThingClassId)) {
+ if (existingDevice->paramValue(bluosPlayerThingSerialNumberParamTypeId).toString() == playerId) {
+ descriptor.setThingId(existingDevice->id());
+ break;
}
}
- info->finish(Thing::ThingErrorNoError);
- });
- return;
- }
- info->finish(Thing::ThingErrorThingClassNotFound);
+ 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 IntegrationPluginDenon::setupThing(ThingSetupInfo *info)
+void IntegrationPluginBluOS::setupThing(ThingSetupInfo *info)
{
Thing *thing = info->thing();
- if (thing->thingClassId() == AVRX1000ThingClassId) {
- qCDebug(dcDenon) << "Setup Denon device" << thing->paramValue(AVRX1000ThingIpParamTypeId).toString();
+ if (thing->thingClassId() == bluosPlayerThingClassId) {
+ qCDebug(dcBluOS()) << "Setup BluOS device" << thing->paramValue(bluosPlayerThingAddressParamTypeId).toString();
- QHostAddress address(thing->paramValue(AVRX1000ThingIpParamTypeId).toString());
- if (address.isNull()) {
- qCWarning(dcDenon) << "Could not parse ip address" << thing->paramValue(AVRX1000ThingIpParamTypeId).toString();
- info->finish(Thing::ThingErrorInvalidParameter, QT_TR_NOOP("The given IP address is not valid."));
- return;
- }
+ 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);
- AvrConnection *denonConnection = new AvrConnection(address, 23, this);
- connect(denonConnection, &AvrConnection::connectionStatusChanged, this, &IntegrationPluginDenon::onAvrConnectionChanged);
- connect(denonConnection, &AvrConnection::socketErrorOccured, this, &IntegrationPluginDenon::onAvrSocketError);
- connect(denonConnection, &AvrConnection::channelChanged, this, &IntegrationPluginDenon::onAvrChannelChanged);
- connect(denonConnection, &AvrConnection::powerChanged, this, &IntegrationPluginDenon::onAvrPowerChanged);
- connect(denonConnection, &AvrConnection::volumeChanged, this, &IntegrationPluginDenon::onAvrVolumeChanged);
- connect(denonConnection, &AvrConnection::surroundModeChanged, this, &IntegrationPluginDenon::onAvrSurroundModeChanged);
- connect(denonConnection, &AvrConnection::muteChanged, this, &IntegrationPluginDenon::onAvrMuteChanged);
-
- m_avrConnections.insert(thing, denonConnection);
- m_asyncAvrSetups.insert(denonConnection, info);
+ m_asyncSetup.insert(bluos, info);
+ bluos->getStatus();
// In case the setup is cancelled before we finish it...
- connect(info, &QObject::destroyed, this, [this, info, denonConnection]() { m_asyncAvrSetups.remove(denonConnection); });
+ connect(info, &QObject::destroyed, this, [this, bluos]() {
+ m_asyncSetup.remove(bluos);
- denonConnection->connectDevice();
+ });
return;
+ } else {
+ return info->finish(Thing::ThingErrorThingClassNotFound);
}
-
- if (thing->thingClassId() == heosThingClassId) {
- qCDebug(dcDenon) << "Setup Denon device" << thing->paramValue(heosThingIpParamTypeId).toString();
-
- QHostAddress address(thing->paramValue(heosThingIpParamTypeId).toString());
- Heos *heos = new Heos(address, this);
- connect(heos, &Heos::connectionStatusChanged, this, &IntegrationPluginDenon::onHeosConnectionChanged);
- connect(heos, &Heos::playerDiscovered, this, &IntegrationPluginDenon::onHeosPlayerDiscovered);
- connect(heos, &Heos::playStateReceived, this, &IntegrationPluginDenon::onHeosPlayStateReceived);
- connect(heos, &Heos::repeatModeReceived, this, &IntegrationPluginDenon::onHeosRepeatModeReceived);
- connect(heos, &Heos::shuffleModeReceived, this, &IntegrationPluginDenon::onHeosShuffleModeReceived);
- connect(heos, &Heos::muteStatusReceived, this, &IntegrationPluginDenon::onHeosMuteStatusReceived);
- connect(heos, &Heos::volumeStatusReceived, this, &IntegrationPluginDenon::onHeosVolumeStatusReceived);
- connect(heos, &Heos::nowPlayingMediaStatusReceived, this, &IntegrationPluginDenon::onHeosNowPlayingMediaStatusReceived);
-
- m_heos.insert(thing, heos);
- m_asyncHeosSetups.insert(heos, info);
- // In case the setup is cancelled before we finish it...
- connect(info, &QObject::destroyed, this, [this, info, heos]() { m_asyncHeosSetups.remove(heos); });
-
- heos->connectHeos();
- return;
- }
-
- if (thing->thingClassId() == heosPlayerThingClassId) {
- info->finish(Thing::ThingErrorNoError);
- return;
- }
- info->finish(Thing::ThingErrorThingClassNotFound);
+}
+
+void IntegrationPluginBluOS::postSetupThing(Thing *thing)
+{
+ Q_UNUSED(thing);
+
+ if (!m_pluginTimer) {
+ //m_pluginTimer = hardwareManager()->pluginTimerManager()->registerTimer(2);
+ //connect(m_pluginTimer, &PluginTimer::timeout, this, &IntegrationPluginBluOS::onPluginTimer);
+ }
+}
+
+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();
+ }
+ 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->setMute(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);
+ }
+ 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)
+{
+ Q_UNUSED(result)
+}
+
+void IntegrationPluginBluOS::browserItem(BrowserItemResult *result)
+{
+ Q_UNUSED(result)
+}
+
+void IntegrationPluginBluOS::executeBrowserItem(BrowserActionInfo *info)
+{
+ Q_UNUSED(info)
+}
+
+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->finish(Thing::ThingErrorNoError);
+ } else {
+ info->finish(Thing::ThingErrorHardwareNotAvailable);
+ }
+ } else {
+ Thing *thing = myThings().findById(m_bluos.key(bluos));
+ if (!thing)
+ 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)
+ return;
+ thing->setStateValue(bluosPlayerArtistStateTypeId, status.Artist);
+ thing->setStateValue(bluosPlayerCollectionStateTypeId, status.Album);
+ thing->setStateValue(bluosPlayerTitleStateTypeId, status.Name);
+ thing->setStateValue(bluosPlayerSourceStateTypeId, status.Service);
+ thing->setStateValue(bluosPlayerArtworkStateTypeId, status.ServiceIcon);
+ thing->setStateValue(bluosPlayerPlaybackStatusStateTypeId, status.PlaybackState);
+ 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;
+ }
+}
+
+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);
+ }
+ }
+}
+
+void IntegrationPluginBluOS::onVolumeReceived(int volume, bool mute)
+{
+ BluOS *bluos = static_cast(sender());
+ Thing *thing = myThings().findById(m_bluos.key(bluos));
+ if (!thing)
+ return;
+ thing->setStateValue(bluosPlayerMuteStateTypeId, mute);
+ thing->setStateValue(bluosPlayerVolumeStateTypeId, volume);
}
diff --git a/bluos/integrationpluginbluos.h b/bluos/integrationpluginbluos.h
index a702db9c..6abd45d0 100644
--- a/bluos/integrationpluginbluos.h
+++ b/bluos/integrationpluginbluos.h
@@ -31,10 +31,13 @@
#ifndef INTEGRATIONPLUGINBLUOS_H
#define INTEGRATIONPLUGINBLUOS_H
+#include "bluos.h"
+
#include "integrations/integrationplugin.h"
+#include "platform/platformzeroconfcontroller.h"
+#include "network/zeroconf/zeroconfservicebrowser.h"
#include
-
#include
class PluginTimer;
@@ -53,17 +56,32 @@ public:
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 refreshStates();
-
-private:
-
-private:
- PluginTimer *m_pollTimer = nullptr;
+ void onPluginTimer();
+ void onConnectionChanged(bool connected);
+ void onStatusResponseReceived(const BluOS::StatusResponse &status);
+ void onActionExecuted(QUuid actionId, bool success);
+ void onVolumeReceived(int volume, bool mute);
};
#endif // INTEGRATIONPLUGINBLUOS_H
diff --git a/bluos/integrationpluginbluos.json b/bluos/integrationpluginbluos.json
index 0baeb342..33e94154 100644
--- a/bluos/integrationpluginbluos.json
+++ b/bluos/integrationpluginbluos.json
@@ -1,40 +1,35 @@
{
"displayName": "BluOS",
- "name": "bluos",
- "id": "",
+ "name": "BluOS",
+ "id": "71dd25b3-37ef-4b27-abca-24989fa38c61",
"vendors": [
{
- "id": "cf0a9644-2c13-4daf-85c1-ad88d6745b42",
- "displayName": "Denon",
- "name": "denon",
+ "id": "39a492e9-e497-4b43-94d4-970eb9913b96",
+ "displayName": "BluOS",
+ "name": "bluos",
"thingClasses": [
{
- "id": "fce5247f-4c6d-408f-ac62-e5973dc6adfa",
- "name": "heosPlayer",
- "displayName": "Heos player",
- "createMethods": ["auto"],
- "interfaces": ["mediaplayer", "mediacontroller", "extendedvolumecontroller", "mediametadataprovider", "shufflerepeat", "connectable"],
+ "id": "406adcbc-1e7d-4e41-ae2a-f87b6bafd13d",
+ "name": "bluosPlayer",
+ "displayName": "BluOS player",
+ "createMethods": ["discovery"],
+ "interfaces": ["extendedvolumecontroller", "mediametadataprovider", "shufflerepeat", "connectable"],
+ "browsable": true,
"paramTypes":[
{
- "id": "89629008-6ad8-4e92-863d-b86e0e012d0b",
- "name": "playerId",
- "displayName": "Player ID",
+ "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": "e760f92b-8fca-4f20-aead-a52045505b81",
- "name": "model",
- "displayName": "Model",
- "type" : "QString"
- },
- {
- "id": "aa1158f7-b451-456a-840f-4f0c63b2b7f0",
- "name": "version",
- "displayName": "Version",
- "type" : "QString"
- },
- {
- "id": "866e8d6a-953f-4bdc-8d85-8d92e51e8592",
+ "id": "8fd2a7d5-bb26-4f18-a488-5ab0779733f8",
"name": "serialNumber",
"displayName": "Serial number",
"type" : "QString"
@@ -42,7 +37,7 @@
],
"stateTypes": [
{
- "id": "9a4e527e-057c-4b19-8a02-605cc8349f5e",
+ "id": "8092f387-5099-43e8-a7c4-7f0dd7b70fce",
"name": "connected",
"displayName": "Connected",
"displayNameEvent": "Connected changed",
@@ -51,7 +46,7 @@
"cached": false
},
{
- "id": "fcc89c7c-b793-4b6f-a3dc-0e0e3a86748f",
+ "id": "2f650ae6-f3a5-4851-8449-1ba02c4864e5",
"name": "mute",
"displayName": "Mute",
"displayNameEvent": "Mute changed",
@@ -62,7 +57,7 @@
"writable": true
},
{
- "id": "6d4886a1-fa5d-4889-96c5-7a1c206f59be",
+ "id": "a444bb7c-7266-4c7e-874a-eb04bb91b9cf",
"name": "volume",
"displayName": "Volume",
"displayNameEvent": "Volume changed",
@@ -74,7 +69,7 @@
"writable": true
},
{
- "id": "6db3b484-4cd4-477b-b822-275865d308db",
+ "id": "43f552c7-7dfe-4358-bebd-ab558191cdfc",
"name": "playbackStatus",
"displayName": "Playback status",
"displayNameEvent": "Playback status changed",
@@ -86,7 +81,7 @@
"writable": true
},
{
- "id": "4b581237-acf5-4d8f-9e83-9b24e9ac900a",
+ "id": "5636c21a-f14b-472c-8486-177543f6adfb",
"name": "shuffle",
"displayName": "Shuffle",
"displayNameEvent": "Shuffle changed",
@@ -97,7 +92,7 @@
"writable": true
},
{
- "id": "4e60cd17-5845-4351-aa2c-2504610e1532",
+ "id": "b4a572b1-6120-43e8-9a6c-18d099c8b162",
"name": "repeat",
"displayName": "Repeat mode",
"displayNameEvent": "Repeat mode changed",
@@ -109,7 +104,7 @@
"writable": true
},
{
- "id": "eee22722-3ee5-48f7-8af8-275dc04b21eb",
+ "id": "604e4995-ee1a-44fc-bfcb-ca0e861710bd",
"name": "source",
"displayName": "Source",
"displayNameEvent": "Source changed",
@@ -117,7 +112,7 @@
"defaultValue": ""
},
{
- "id": "0a9183a4-b633-4773-ba7a-f4266895157e",
+ "id": "8c5372f1-3dde-4984-9955-9a436a29e8e3",
"name": "artist",
"displayName": "Artist",
"displayNameEvent": "Artist changed",
@@ -125,7 +120,7 @@
"defaultValue": ""
},
{
- "id": "9cd60864-f141-4e03-a85b-357690cad1b8",
+ "id": "d2a2db24-5855-40cd-a043-6f67e43acc61",
"name": "collection",
"displayName": "Album",
"displayNameEvent": "Album changed",
@@ -133,7 +128,7 @@
"defaultValue": ""
},
{
- "id": "bbeecf30-6feb-48d5-ade3-57b2a4eea05f",
+ "id": "6204ec9d-6aab-4fd3-8911-fdaa462c19a8",
"name": "title",
"displayName": "Title",
"displayNameEvent": "Title changed",
@@ -141,49 +136,37 @@
"defaultValue": ""
},
{
- "id": "a7f0ba95-383a-4efd-adc5-a36e50a04018",
+ "id": "918bfeed-bf96-4034-85d6-9f4cff35fd00",
"name": "artwork",
"displayName": "Artwork",
"displayNameEvent": "Artwork changed",
"type": "QString",
"defaultValue": ""
- },
- {
- "id": "c59835ac-ee6e-4e6c-aa20-aeb3501937c5",
- "name": "playerType",
- "displayName": "Player type",
- "displayNameEvent": "Player type changed",
- "possibleValues": [
- "audio",
- "video"
- ],
- "type": "QString",
- "defaultValue": "audio"
}
],
"actionTypes": [
{
- "id": "a718f7e9-0b54-4403-b661-49f7b0d13085",
+ "id": "864a5bcc-71e1-4d7f-8f2e-3c4222d5d988",
"name": "skipBack",
- "displayName": "Akip back"
+ "displayName": "Skip back"
},
{
- "id": "c4b29c09-e3b3-4843-b6d9-e032f3fc1d78",
+ "id": "2526ec6d-1c21-4f73-97f1-973a5f05b626",
"name": "stop",
"displayName": "Stop"
},
{
- "id": "c64964e4-cea0-468a-a9bf-8f69657b74e9",
+ "id": "253eb62f-d50d-4667-8213-8632de178aa3",
"name": "play",
"displayName": "Play"
},
{
- "id": "21c1cbe6-278f-4688-a65f-6620be1ee5ea",
+ "id": "92446566-8c32-4ee2-9498-d9dd9333a75d",
"name": "pause",
"displayName": "Pause"
},
{
- "id": "57697e9c-ce5e-4b8f-b42e-16662829ceb2",
+ "id": "30930095-6f97-48ef-8bbf-597d734f0751",
"name": "skipNext",
"displayName": "Skip next"
}