discovery and basic control works nor

master
bernhard.trinnes 2020-03-16 22:03:42 +01:00
parent 6a199f5ae8
commit c2aa46d6e3
5 changed files with 891 additions and 160 deletions

View File

@ -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 <https://www.gnu.org/licenses/>.
*
* 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 <QNetworkRequest>
#include <QNetworkReply>
#include <QUrlQuery>
#include <QXmlStreamReader>
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;
}

View File

@ -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 <https://www.gnu.org/licenses/>.
*
* 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 <QObject>
#include <QTimer>
#include <QHostAddress>
#include <QUuid>
#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<Preset> &presets);
void statusReceived(const StatusResponse &status);
void volumeReceived(int volume, bool mute);
};
#endif // BLUOS_H

View File

@ -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 <QDebug>
#include <QStringList>
@ -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<BluOS*>(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<BluOS*>(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<BluOS*>(sender());
Thing *thing = myThings().findById(m_bluos.key(bluos));
if (!thing)
return;
thing->setStateValue(bluosPlayerMuteStateTypeId, mute);
thing->setStateValue(bluosPlayerVolumeStateTypeId, volume);
}

View File

@ -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 <QUdpSocket>
#include <QNetworkAccessManager>
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<ThingId, BluOS *> m_bluos;
QHash<BluOS *, ThingSetupInfo *> m_asyncSetup;
QHash<QUuid, ThingActionInfo *> m_asyncActions;
QHash<QUuid, BrowseResult *> m_asyncBrowseResults;
QHash<QUuid, BrowserActionInfo *> m_asyncExecuteBrowseItems;
QHash<QUuid, BrowserItemResult *> 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

View File

@ -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"
}