From c2aa46d6e346d234e7a5f2470708e40b95885adb Mon Sep 17 00:00:00 2001 From: "bernhard.trinnes" Date: Mon, 16 Mar 2020 22:03:42 +0100 Subject: [PATCH] discovery and basic control works nor --- bluos/bluos.cpp | 481 ++++++++++++++++++++++++++++++ bluos/bluos.h | 127 ++++++++ bluos/integrationpluginbluos.cpp | 318 ++++++++++++++------ bluos/integrationpluginbluos.h | 32 +- bluos/integrationpluginbluos.json | 93 +++--- 5 files changed, 891 insertions(+), 160 deletions(-) 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" }