From 49907ecb895212f2a26a310b33c92e6d5e182002 Mon Sep 17 00:00:00 2001 From: "bernhard.trinnes" Date: Fri, 13 Mar 2020 12:03:56 +0100 Subject: [PATCH 01/14] added bluos integration plugin --- debian/control | 15 +++++++++++++++ nymea-plugins.pro | 1 + 2 files changed, 16 insertions(+) diff --git a/debian/control b/debian/control index 6efef76e..b3d37b62e 100644 --- a/debian/control +++ b/debian/control @@ -72,6 +72,21 @@ Description: nymea.io plugin for awattar This package will install the nymea.io plugin for awattar +Package: nymea-plugin-bluos +Architecture: any +Depends: ${shlibs:Depends}, + ${misc:Depends}, + nymea-plugins-translations, +Description: nymea.io plugin for bluos + The nymea daemon is a plugin based IoT (Internet of Things) server. The + server works like a translator for devices, things and services and + allows them to interact. + With the powerful rule engine you are able to connect any device available + in the system and create individual scenes and behaviors for your environment. + . + This package will install the nymea.io plugin for bluos + + Package: nymea-plugin-boblight Architecture: any Depends: ${shlibs:Depends}, diff --git a/nymea-plugins.pro b/nymea-plugins.pro index 23d78cdf..263579b6 100644 --- a/nymea-plugins.pro +++ b/nymea-plugins.pro @@ -4,6 +4,7 @@ PLUGIN_DIRS = \ anel \ avahimonitor \ awattar \ + bluos \ boblight \ bose \ coinmarketcap \ From 6a199f5ae8f4c66a26db410cd860fd3b5f7efe1e Mon Sep 17 00:00:00 2001 From: "bernhard.trinnes" Date: Fri, 13 Mar 2020 12:54:42 +0100 Subject: [PATCH 02/14] added bluos files --- bluos/README.md | 0 bluos/bluos.cpp | 0 bluos/bluos.h | 0 bluos/bluos.pro | 13 ++ bluos/integrationpluginbluos.cpp | 172 +++++++++++++++++++++++ bluos/integrationpluginbluos.h | 69 ++++++++++ bluos/integrationpluginbluos.json | 195 +++++++++++++++++++++++++++ debian/nymea-plugin-bluos.install.in | 1 + 8 files changed, 450 insertions(+) create mode 100644 bluos/README.md create mode 100644 bluos/bluos.cpp create mode 100644 bluos/bluos.h create mode 100644 bluos/bluos.pro create mode 100644 bluos/integrationpluginbluos.cpp create mode 100644 bluos/integrationpluginbluos.h create mode 100644 bluos/integrationpluginbluos.json create mode 100644 debian/nymea-plugin-bluos.install.in diff --git a/bluos/README.md b/bluos/README.md new file mode 100644 index 00000000..e69de29b diff --git a/bluos/bluos.cpp b/bluos/bluos.cpp new file mode 100644 index 00000000..e69de29b diff --git a/bluos/bluos.h b/bluos/bluos.h new file mode 100644 index 00000000..e69de29b diff --git a/bluos/bluos.pro b/bluos/bluos.pro new file mode 100644 index 00000000..a878a4c7 --- /dev/null +++ b/bluos/bluos.pro @@ -0,0 +1,13 @@ +include(../plugins.pri) + +QT += network + +TARGET = $$qtLibraryTarget(nymea_integrationpluginbluos) + +SOURCES += \ + integrationpluginbluos.cpp \ + bluos.cpp \ + +HEADERS += \ + integrationpluginbluos.h \ + bluos.h \ diff --git a/bluos/integrationpluginbluos.cpp b/bluos/integrationpluginbluos.cpp new file mode 100644 index 00000000..958b3017 --- /dev/null +++ b/bluos/integrationpluginbluos.cpp @@ -0,0 +1,172 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* Copyright 2013 - 2020, nymea GmbH +* Contact: contact@nymea.io +* +* This file is part of nymea. +* This project including source code and documentation is protected by +* copyright law, and remains the property of nymea GmbH. All rights, including +* reproduction, publication, editing and translation, are reserved. The use of +* this project is subject to the terms of a license agreement to be concluded +* with nymea GmbH in accordance with the terms of use of nymea GmbH, available +* under https://nymea.io/license +* +* GNU Lesser General Public License Usage +* Alternatively, this project may be redistributed and/or modified under the +* terms of the GNU Lesser General Public License as published by the Free +* Software Foundation; version 3. This project is distributed in the hope that +* it will be useful, but WITHOUT ANY WARRANTY; without even the implied +* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +* Lesser General Public License for more details. +* +* You should have received a copy of the GNU Lesser General Public License +* along with this project. If not, see . +* +* For any further details and any questions please contact us under +* contact@nymea.io or see our FAQ/Licensing Information on +* https://nymea.io/license/faq +* +* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + + +#include "integrationpluginbluos.h" +#include "plugininfo.h" +#include "integrations/thing.h" +#include "network/networkaccessmanager.h" +#include "platform/platformzeroconfcontroller.h" +#include "network/zeroconf/zeroconfservicebrowser.h" + +#include +#include +#include +#include + + +IntegrationPluginBluOS::IntegrationPluginBluOS() +{ + +} + +IntegrationPluginBluOS::~IntegrationPluginBluOS() +{ + +} + +void IntegrationPluginBluOS::init() +{ + +} + +void IntegrationPluginBluOS::discoverThings(ThingDiscoveryInfo *info) +{ + + 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(); + + 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); + } + } + } + info->finish(Thing::ThingErrorNoError); + }); + return; + } + info->finish(Thing::ThingErrorThingClassNotFound); +} + +void IntegrationPluginDenon::setupThing(ThingSetupInfo *info) +{ + Thing *thing = info->thing(); + + if (thing->thingClassId() == AVRX1000ThingClassId) { + qCDebug(dcDenon) << "Setup Denon device" << thing->paramValue(AVRX1000ThingIpParamTypeId).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; + } + + 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); + // In case the setup is cancelled before we finish it... + connect(info, &QObject::destroyed, this, [this, info, denonConnection]() { m_asyncAvrSetups.remove(denonConnection); }); + + denonConnection->connectDevice(); + return; + } + + 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); +} diff --git a/bluos/integrationpluginbluos.h b/bluos/integrationpluginbluos.h new file mode 100644 index 00000000..a702db9c --- /dev/null +++ b/bluos/integrationpluginbluos.h @@ -0,0 +1,69 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* Copyright 2013 - 2020, nymea GmbH +* Contact: contact@nymea.io +* +* This file is part of nymea. +* This project including source code and documentation is protected by +* copyright law, and remains the property of nymea GmbH. All rights, including +* reproduction, publication, editing and translation, are reserved. The use of +* this project is subject to the terms of a license agreement to be concluded +* with nymea GmbH in accordance with the terms of use of nymea GmbH, available +* under https://nymea.io/license +* +* GNU Lesser General Public License Usage +* Alternatively, this project may be redistributed and/or modified under the +* terms of the GNU Lesser General Public License as published by the Free +* Software Foundation; version 3. This project is distributed in the hope that +* it will be useful, but WITHOUT ANY WARRANTY; without even the implied +* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +* Lesser General Public License for more details. +* +* You should have received a copy of the GNU Lesser General Public License +* along with this project. If not, see . +* +* For any further details and any questions please contact us under +* contact@nymea.io or see our FAQ/Licensing Information on +* https://nymea.io/license/faq +* +* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +#ifndef INTEGRATIONPLUGINBLUOS_H +#define INTEGRATIONPLUGINBLUOS_H + +#include "integrations/integrationplugin.h" + +#include + +#include + +class PluginTimer; + +class IntegrationPluginBluOS: public IntegrationPlugin +{ + Q_OBJECT + + Q_PLUGIN_METADATA(IID "io.nymea.IntegrationPlugin" FILE "integrationpluginbluos.json") + Q_INTERFACES(IntegrationPlugin) + +public: + explicit IntegrationPluginBluOS(); + ~IntegrationPluginBluOS(); + + void init() override; + void discoverThings(ThingDiscoveryInfo *info) override; + void setupThing(ThingSetupInfo *info) override; + void thingRemoved(Thing *thing) override; + void executeAction(ThingActionInfo *info) override; + +private slots: + void refreshStates(); + +private: + +private: + PluginTimer *m_pollTimer = nullptr; + +}; + +#endif // INTEGRATIONPLUGINBLUOS_H diff --git a/bluos/integrationpluginbluos.json b/bluos/integrationpluginbluos.json new file mode 100644 index 00000000..0baeb342 --- /dev/null +++ b/bluos/integrationpluginbluos.json @@ -0,0 +1,195 @@ +{ + "displayName": "BluOS", + "name": "bluos", + "id": "", + "vendors": [ + { + "id": "cf0a9644-2c13-4daf-85c1-ad88d6745b42", + "displayName": "Denon", + "name": "denon", + "thingClasses": [ + { + "id": "fce5247f-4c6d-408f-ac62-e5973dc6adfa", + "name": "heosPlayer", + "displayName": "Heos player", + "createMethods": ["auto"], + "interfaces": ["mediaplayer", "mediacontroller", "extendedvolumecontroller", "mediametadataprovider", "shufflerepeat", "connectable"], + "paramTypes":[ + { + "id": "89629008-6ad8-4e92-863d-b86e0e012d0b", + "name": "playerId", + "displayName": "Player ID", + "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", + "name": "serialNumber", + "displayName": "Serial number", + "type" : "QString" + } + ], + "stateTypes": [ + { + "id": "9a4e527e-057c-4b19-8a02-605cc8349f5e", + "name": "connected", + "displayName": "Connected", + "displayNameEvent": "Connected changed", + "type": "bool", + "defaultValue": false, + "cached": false + }, + { + "id": "fcc89c7c-b793-4b6f-a3dc-0e0e3a86748f", + "name": "mute", + "displayName": "Mute", + "displayNameEvent": "Mute changed", + "displayNameAction": "Set mute", + "type": "bool", + "defaultValue": false, + "cached": false, + "writable": true + }, + { + "id": "6d4886a1-fa5d-4889-96c5-7a1c206f59be", + "name": "volume", + "displayName": "Volume", + "displayNameEvent": "Volume changed", + "displayNameAction": "Set volume", + "type": "int", + "defaultValue": 50, + "minValue": 0, + "maxValue": 100, + "writable": true + }, + { + "id": "6db3b484-4cd4-477b-b822-275865d308db", + "name": "playbackStatus", + "displayName": "Playback status", + "displayNameEvent": "Playback status changed", + "displayNameAction": "Set playback status", + "type": "QString", + "defaultValue": "Stopped", + "possibleValues": ["Playing", "Paused", "Stopped"], + "cached": false, + "writable": true + }, + { + "id": "4b581237-acf5-4d8f-9e83-9b24e9ac900a", + "name": "shuffle", + "displayName": "Shuffle", + "displayNameEvent": "Shuffle changed", + "displayNameAction": "Set shuffle", + "type": "bool", + "defaultValue": false, + "cached": false, + "writable": true + }, + { + "id": "4e60cd17-5845-4351-aa2c-2504610e1532", + "name": "repeat", + "displayName": "Repeat mode", + "displayNameEvent": "Repeat mode changed", + "displayNameAction": "Set repeat mode", + "type": "QString", + "defaultValue": "None", + "possibleValues": ["None", "One", "All"], + "cached": false, + "writable": true + }, + { + "id": "eee22722-3ee5-48f7-8af8-275dc04b21eb", + "name": "source", + "displayName": "Source", + "displayNameEvent": "Source changed", + "type": "QString", + "defaultValue": "" + }, + { + "id": "0a9183a4-b633-4773-ba7a-f4266895157e", + "name": "artist", + "displayName": "Artist", + "displayNameEvent": "Artist changed", + "type": "QString", + "defaultValue": "" + }, + { + "id": "9cd60864-f141-4e03-a85b-357690cad1b8", + "name": "collection", + "displayName": "Album", + "displayNameEvent": "Album changed", + "type": "QString", + "defaultValue": "" + }, + { + "id": "bbeecf30-6feb-48d5-ade3-57b2a4eea05f", + "name": "title", + "displayName": "Title", + "displayNameEvent": "Title changed", + "type": "QString", + "defaultValue": "" + }, + { + "id": "a7f0ba95-383a-4efd-adc5-a36e50a04018", + "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", + "name": "skipBack", + "displayName": "Akip back" + }, + { + "id": "c4b29c09-e3b3-4843-b6d9-e032f3fc1d78", + "name": "stop", + "displayName": "Stop" + }, + { + "id": "c64964e4-cea0-468a-a9bf-8f69657b74e9", + "name": "play", + "displayName": "Play" + }, + { + "id": "21c1cbe6-278f-4688-a65f-6620be1ee5ea", + "name": "pause", + "displayName": "Pause" + }, + { + "id": "57697e9c-ce5e-4b8f-b42e-16662829ceb2", + "name": "skipNext", + "displayName": "Skip next" + } + ] + } + ] + } + ] +} diff --git a/debian/nymea-plugin-bluos.install.in b/debian/nymea-plugin-bluos.install.in new file mode 100644 index 00000000..11c5b050 --- /dev/null +++ b/debian/nymea-plugin-bluos.install.in @@ -0,0 +1 @@ +usr/lib/@DEB_HOST_MULTIARCH@/nymea/plugins/libnymea_devicepluginbluos.so From c2aa46d6e346d234e7a5f2470708e40b95885adb Mon Sep 17 00:00:00 2001 From: "bernhard.trinnes" Date: Mon, 16 Mar 2020 22:03:42 +0100 Subject: [PATCH 03/14] 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" } From c1bc56d84a3d562472336a37d00e8a0f3c18a82e Mon Sep 17 00:00:00 2001 From: "bernhard.trinnes" Date: Tue, 17 Mar 2020 09:16:24 +0100 Subject: [PATCH 04/14] added meta data --- bluos/bluos.cpp | 181 ++++++++++++++++++++++--------- bluos/bluos.h | 31 +++++- bluos/integrationpluginbluos.cpp | 136 ++++++++++++++++++++--- bluos/integrationpluginbluos.h | 4 + 4 files changed, 283 insertions(+), 69 deletions(-) diff --git a/bluos/bluos.cpp b/bluos/bluos.cpp index bda017eb..b6251496 100644 --- a/bluos/bluos.cpp +++ b/bluos/bluos.cpp @@ -77,44 +77,9 @@ QUuid BluOS::getStatus() 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); + QByteArray data = reply->readAll(); + qCDebug(dcBluOS()) << "Get Status:" << data; + parseState(data); }); return requestId; } @@ -303,6 +268,68 @@ QUuid BluOS::listPresets() url.setPort(m_port); url.setPath("/Presets"); 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); + } + + 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(); + return; + } + QList presetList; + if (xml.readNextStartElement()) { + if (xml.name() == "presets") { + while(xml.readNextStartElement()){ + if(xml.name() == "preset"){ + Preset preset; + if (xml.attributes().hasAttribute("id")) { + preset.Id = xml.attributes().value("id").toInt(); + } + if (xml.attributes().hasAttribute("name")) { + preset.Name = xml.attributes().value("name").toString(); + } + if (xml.attributes().hasAttribute("url")) { + preset.Url = xml.attributes().value("url").toString(); + } + presetList.append(preset); + } else { + xml.skipCurrentElement(); + } + } + } + } + emit presetsReceived(requestId, presetList); + }); + + return requestId; +} + +QUuid BluOS::loadPreset(int preset) +{ + QUuid requestId = QUuid::createUuid(); + + QUrl url; + url.setScheme("http"); + url.setHost(m_hostAddress.toString()); + url.setPort(m_port); + url.setPath("/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(); @@ -318,23 +345,18 @@ QUuid BluOS::listPresets() } emit connectionChanged(true); }); - return requestId; } -QUuid BluOS::loadPreset(int preset) +QUuid BluOS::getSources() { - 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); + url.setPath("/Browse"); QNetworkReply *reply = m_networkManager->get(QNetworkRequest(url)); connect(reply, &QNetworkReply::finished, this, [reply, this] { reply->deleteLater(); @@ -470,12 +492,69 @@ QUuid BluOS::playBackControl(BluOS::PlaybackCommand command) } emit connectionChanged(true); - QXmlStreamReader xml; - xml.addData(reply->readAll()); - if (xml.hasError()) { - qCDebug(dcBluOS()) << "XML Error:" << xml.errorString(); - } - emit actionExecuted(requestId, true); + QByteArray data = reply->readAll(); + parseState(data); }); return requestId; } + +bool BluOS::parseState(const QByteArray &state) +{ + QXmlStreamReader xml; + xml.addData(state); + if (xml.hasError()) { + qCDebug(dcBluOS()) << "XML Error:" << xml.errorString(); + return false; + } + + StatusResponse statusResponse; + if (xml.readNextStartElement()) { + if (xml.name() == "status") { + while(xml.readNextStartElement()){ + if(xml.name() == "artist"){ + statusResponse.Artist = xml.readElementText(); + } else if(xml.name() == "album"){ + statusResponse.Album = xml.readElementText(); + } else if(xml.name() == "name"){ + statusResponse.Name = xml.readElementText(); + } else if(xml.name() == "service"){ + statusResponse.Service = xml.readElementText(); + } else if(xml.name() == "serviceIcon"){ + statusResponse.ServiceIcon = xml.readElementText(); + } else if(xml.name() == "shuffle"){ + statusResponse.Shuffle = xml.readElementText().toInt(); + } else if(xml.name() == "repeat"){ + statusResponse.Shuffle = xml.readElementText().toInt(); + } else if(xml.name() == "state"){ + QString playback = xml.readElementText(); + if (playback == "play") { + statusResponse.State = PlaybackState::Playing; + } else if (playback == "pause") { + statusResponse.State = PlaybackState::Paused; + } else if (playback == "stop") { + statusResponse.State = PlaybackState::Stopped; + } else if (playback == "connecting") { + statusResponse.State = PlaybackState::Connecting; + } else if (playback == "stream") { + statusResponse.State = PlaybackState::Streaming; + } else { + statusResponse.State = PlaybackState::Stopped; + qCWarning(dcBluOS()) << "State response, unhandled playback mode" << playback; + } + } else if(xml.name() == "volume"){ + statusResponse.Volume = xml.readElementText().toInt(); + } else if(xml.name() == "mute"){ + statusResponse.Mute = xml.readElementText().toInt(); + } else if(xml.name() == "image") { + statusResponse.Image = xml.readElementText(); + } else if(xml.name() == "title1") { + statusResponse.Title = xml.readElementText(); + } else { + xml.skipCurrentElement(); + } + } + } + } + emit statusReceived(statusResponse); + return true; +} diff --git a/bluos/bluos.h b/bluos/bluos.h index 59631aca..0dacabcd 100644 --- a/bluos/bluos.h +++ b/bluos/bluos.h @@ -58,25 +58,42 @@ public: None }; + enum PlaybackState { + Playing, + Paused, + Stopped, + Connecting, + Streaming + }; + struct StatusResponse { QString Album; QString Artist; QString Name; + QString Title; QString Service; QUrl ServiceIcon; - QString PlaybackState; + PlaybackState State; QUrl StationUrl; int Volume; bool Mute; RepeatMode Repeat; bool Shuffle; + QUrl Image; }; struct Preset { - int Prid; - QString name; + QString Name; int Id; - QString url; + QString Url; + }; + + struct Source { + // + QString Image; + QString BrowseKey; + QString Text; + QString Type; }; explicit BluOS(NetworkAccessManager *networkManager, QHostAddress hostAddress, int port, QObject *parent = nullptr); @@ -104,6 +121,7 @@ public: QUuid loadPreset(int preset); //1 for next preset, -1 for previous preset // Content Browsing + QUuid getSources(); // Player Grouping QUuid addGroupPlayer(QHostAddress address, int port); //adds player as slave @@ -115,13 +133,16 @@ private: NetworkAccessManager *m_networkManager = nullptr; QUuid playBackControl(PlaybackCommand command); + bool parseState(const QByteArray &state); signals: void connectionChanged(bool connected); void actionExecuted(QUuid actionId, bool success); - void presetsReceived(const QList &presets); void statusReceived(const StatusResponse &status); void volumeReceived(int volume, bool mute); + + void presetsReceived(QUuid requestId, const QList &presets); + void sourcesReceived(QUuid requestId, const QList &sources); }; #endif // BLUOS_H diff --git a/bluos/integrationpluginbluos.cpp b/bluos/integrationpluginbluos.cpp index f7a0efe3..6b229152 100644 --- a/bluos/integrationpluginbluos.cpp +++ b/bluos/integrationpluginbluos.cpp @@ -33,7 +33,8 @@ #include "plugininfo.h" #include "integrations/thing.h" #include "network/networkaccessmanager.h" - +#include "types/mediabrowseritem.h" +#include "types/browseritem.h" #include #include @@ -95,6 +96,9 @@ void IntegrationPluginBluOS::setupThing(ThingSetupInfo *info) connect(bluos, &BluOS::connectionChanged, this, &IntegrationPluginBluOS::onConnectionChanged); connect(bluos, &BluOS::statusReceived, this, &IntegrationPluginBluOS::onStatusResponseReceived); connect(bluos, &BluOS::actionExecuted, this, &IntegrationPluginBluOS::onActionExecuted); + connect(bluos, &BluOS::volumeReceived, this, &IntegrationPluginBluOS::onVolumeReceived); + connect(bluos, &BluOS::presetsReceived, this, &IntegrationPluginBluOS::onPresetsReceived); + connect(bluos, &BluOS::sourcesReceived, this, &IntegrationPluginBluOS::onSourcesReceived); m_asyncSetup.insert(bluos, info); bluos->getStatus(); @@ -114,8 +118,12 @@ 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); + m_pluginTimer = hardwareManager()->pluginTimerManager()->registerTimer(10); + connect(m_pluginTimer, &PluginTimer::timeout, [this] { + foreach(BluOS *bluos, m_bluos) { + bluos->getStatus(); + } + }); } } @@ -148,6 +156,8 @@ void IntegrationPluginBluOS::executeAction(ThingActionInfo *info) requestId = bluos->pause(); } else if (playbakStatus == "Stopped") { requestId = bluos->stop(); + } else { + qCWarning(dcBluOS()) << "Unhandled Playback mode"; } m_asyncActions.insert(requestId, info); connect(info, &ThingActionInfo::aborted, [this, requestId] {m_asyncActions.remove(requestId);}); @@ -183,7 +193,7 @@ void IntegrationPluginBluOS::executeAction(ThingActionInfo *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); + QUuid requestId = bluos->setShuffle(shuffle); m_asyncActions.insert(requestId, info); connect(info, &ThingActionInfo::aborted, [this, requestId] {m_asyncActions.remove(requestId);}); } else if (action.actionTypeId() == bluosPlayerRepeatActionTypeId) { @@ -195,6 +205,8 @@ void IntegrationPluginBluOS::executeAction(ThingActionInfo *info) requestId = bluos->setRepeat(BluOS::RepeatMode::All); } else if (repeat == "None") { requestId = bluos->setRepeat(BluOS::RepeatMode::None); + } else { + qCWarning(dcBluOS()) << "Unhandled Repeat Mode"; } m_asyncActions.insert(requestId, info); connect(info, &ThingActionInfo::aborted, [this, requestId] {m_asyncActions.remove(requestId);}); @@ -210,12 +222,54 @@ void IntegrationPluginBluOS::executeAction(ThingActionInfo *info) void IntegrationPluginBluOS::browseThing(BrowseResult *result) { - Q_UNUSED(result) + Thing *thing = result->thing(); + if (thing->thingClassId() == bluosPlayerThingClassId) { + BluOS *bluos = m_bluos.value(thing->id()); + if (!bluos) { + return; + } + if (result->itemId() == "presets") { + QUuid requestId = bluos->listPresets(); + m_asyncBrowseResults.insert(requestId, result); + connect(result, &BrowseResult::aborted, this, [this, requestId]{m_asyncBrowseResults.remove(requestId);}); + } else { + MediaBrowserItem presetItem("presets", "Presets", true, false); + presetItem.setIcon(BrowserItem::BrowserIcon::BrowserIconFavorites); + presetItem.setMediaIcon(MediaBrowserItem::MediaBrowserIconMusicLibrary); + result->addItem(presetItem); + + MediaBrowserItem groupingItem("grouping", "Grouping", true, false); + presetItem.setIcon(BrowserItem::BrowserIcon::BrowserIconApplication); + presetItem.setMediaIcon(MediaBrowserItem::MediaBrowserIconNetwork); + result->addItem(presetItem); + + QUuid requestId = bluos->getSources(); + m_asyncBrowseResults.insert(requestId, result); + connect(result, &BrowseResult::aborted, this, [this, requestId]{m_asyncBrowseResults.remove(requestId);}); + } + } } void IntegrationPluginBluOS::browserItem(BrowserItemResult *result) { - Q_UNUSED(result) + Thing *thing = result->thing(); + if (thing->thingClassId() == bluosPlayerThingClassId) { + BluOS *bluos = m_bluos.value(thing->id()); + if (!bluos) { + return; + } + if (result->itemId() == "presets") { + QUuid requestId = bluos->listPresets(); + m_asyncBrowseItemResults.insert(requestId, result); + connect(result, &BrowserItemResult::aborted, this, [this, requestId]{m_asyncBrowseItemResults.remove(requestId);}); + } else { + BrowserItem presetItem("presets", "Presets", true, false); + presetItem.setIcon(BrowserItem::BrowserIcon::BrowserIconFavorites); + QUuid requestId = bluos->getSources(); + m_asyncBrowseItemResults.insert(requestId, result); + connect(result, &BrowserItemResult::aborted, this, [this, requestId]{m_asyncBrowseItemResults.remove(requestId);}); + } + } } void IntegrationPluginBluOS::executeBrowserItem(BrowserActionInfo *info) @@ -231,9 +285,10 @@ void IntegrationPluginBluOS::onConnectionChanged(bool connected) ThingSetupInfo *info = m_asyncSetup.take(bluos); if (connected) { m_bluos.insert(info->thing()->id(), bluos); + info->thing()->setStateValue(bluosPlayerConnectedStateTypeId, true); info->finish(Thing::ThingErrorNoError); } else { - info->finish(Thing::ThingErrorHardwareNotAvailable); + info->finish(Thing::ThingErrorSetupFailed); } } else { Thing *thing = myThings().findById(m_bluos.key(bluos)); @@ -251,23 +306,38 @@ void IntegrationPluginBluOS::onStatusResponseReceived(const BluOS::StatusRespons return; thing->setStateValue(bluosPlayerArtistStateTypeId, status.Artist); thing->setStateValue(bluosPlayerCollectionStateTypeId, status.Album); - thing->setStateValue(bluosPlayerTitleStateTypeId, status.Name); + thing->setStateValue(bluosPlayerTitleStateTypeId, status.Title); thing->setStateValue(bluosPlayerSourceStateTypeId, status.Service); - thing->setStateValue(bluosPlayerArtworkStateTypeId, status.ServiceIcon); - thing->setStateValue(bluosPlayerPlaybackStatusStateTypeId, status.PlaybackState); + thing->setStateValue(bluosPlayerArtworkStateTypeId, status.Image); + switch (status.State) { + case BluOS::PlaybackState::Playing: + case BluOS::PlaybackState::Streaming: + thing->setStateValue(bluosPlayerPlaybackStatusStateTypeId, "Playing"); + break; + case BluOS::PlaybackState::Paused: + thing->setStateValue(bluosPlayerPlaybackStatusStateTypeId, "Paused"); + break; + case BluOS::PlaybackState::Stopped: + thing->setStateValue(bluosPlayerPlaybackStatusStateTypeId, "Stopped"); + break; + default: + thing->setStateValue(bluosPlayerPlaybackStatusStateTypeId, "Stopped"); + break; + } + thing->setStateValue(bluosPlayerMuteStateTypeId, status.Mute); thing->setStateValue(bluosPlayerVolumeStateTypeId, status.Volume); thing->setStateValue(bluosPlayerShuffleStateTypeId, status.Shuffle); switch (status.Repeat) { - case BluOS::RepeatMode::All: + case BluOS::RepeatMode::All: thing->setStateValue(bluosPlayerRepeatStateTypeId, "All"); break; case BluOS::RepeatMode::One: thing->setStateValue(bluosPlayerRepeatStateTypeId, "One"); - break; + break; case BluOS::RepeatMode::None: thing->setStateValue(bluosPlayerRepeatStateTypeId, "None"); - break; + break; } } @@ -292,3 +362,43 @@ void IntegrationPluginBluOS::onVolumeReceived(int volume, bool mute) thing->setStateValue(bluosPlayerMuteStateTypeId, mute); thing->setStateValue(bluosPlayerVolumeStateTypeId, volume); } + +void IntegrationPluginBluOS::onPresetsReceived(QUuid requestId, const QList &presets) +{ + BluOS *bluos = static_cast(sender()); + Thing *thing = myThings().findById(m_bluos.key(bluos)); + if (!thing) + return; + Q_UNUSED(presets) + if (m_asyncBrowseResults.contains(requestId)) { + BrowseResult *result = m_asyncBrowseResults.take(requestId); + foreach(BluOS::Preset preset, presets) { + BrowserItem item("presets&"+QString::number(preset.Id), preset.Name, false, true); + item.setIcon(BrowserItem::BrowserIcon::BrowserIconFavorites); + result->addItem(item); + } + result->finish(Thing::ThingErrorNoError); + } + if (m_asyncBrowseItemResults.contains(requestId)) { + BrowserItemResult *result = m_asyncBrowseItemResults.take(requestId); + Q_UNUSED(result) + } +} + +void IntegrationPluginBluOS::onSourcesReceived(QUuid requestId, const QList &sources) +{ + BluOS *bluos = static_cast(sender()); + Thing *thing = myThings().findById(m_bluos.key(bluos)); + if (!thing) + return; + if (m_asyncBrowseResults.contains(requestId)) { + BrowseResult *result = m_asyncBrowseResults.take(requestId); + foreach(BluOS::Source source, sources) { + BrowserItem item(source.BrowseKey, source.Text, false, true); + item.setIcon(BrowserItem::BrowserIcon::BrowserIconFavorites); + //TODO set media icons + result->addItem(item); + } + result->finish(Thing::ThingErrorNoError); + } +} diff --git a/bluos/integrationpluginbluos.h b/bluos/integrationpluginbluos.h index 6abd45d0..b027d7d1 100644 --- a/bluos/integrationpluginbluos.h +++ b/bluos/integrationpluginbluos.h @@ -36,6 +36,7 @@ #include "integrations/integrationplugin.h" #include "platform/platformzeroconfcontroller.h" #include "network/zeroconf/zeroconfservicebrowser.h" +#include "plugintimer.h" #include #include @@ -82,6 +83,9 @@ private slots: void onStatusResponseReceived(const BluOS::StatusResponse &status); void onActionExecuted(QUuid actionId, bool success); void onVolumeReceived(int volume, bool mute); + + void onPresetsReceived(QUuid requestId, const QList &presets); + void onSourcesReceived(QUuid requestId, const QList &sources); }; #endif // INTEGRATIONPLUGINBLUOS_H From 5d9b6c55e15e933f422c67576748dde4fd4c04d3 Mon Sep 17 00:00:00 2001 From: "bernhard.trinnes" Date: Tue, 17 Mar 2020 11:44:50 +0100 Subject: [PATCH 05/14] added first level browsing --- bluos/bluos.cpp | 127 ++++++++++++++++++++++++++++--- bluos/bluos.h | 2 + bluos/integrationpluginbluos.cpp | 106 +++++++++++++++++++++++--- bluos/integrationpluginbluos.h | 2 +- 4 files changed, 215 insertions(+), 22 deletions(-) diff --git a/bluos/bluos.cpp b/bluos/bluos.cpp index b6251496..64875f27 100644 --- a/bluos/bluos.cpp +++ b/bluos/bluos.cpp @@ -78,7 +78,7 @@ QUuid BluOS::getStatus() } emit connectionChanged(true); QByteArray data = reply->readAll(); - qCDebug(dcBluOS()) << "Get Status:" << data; + //qCDebug(dcBluOS()) << "Get Status:" << data; parseState(data); }); return requestId; @@ -283,8 +283,9 @@ QUuid BluOS::listPresets() } emit connectionChanged(true); + QByteArray data = reply->readAll(); QXmlStreamReader xml; - xml.addData(reply->readAll()); + xml.addData(data); if (xml.hasError()) { qCDebug(dcBluOS()) << "XML Error:" << xml.errorString(); return; @@ -298,12 +299,13 @@ QUuid BluOS::listPresets() if (xml.attributes().hasAttribute("id")) { preset.Id = xml.attributes().value("id").toInt(); } - if (xml.attributes().hasAttribute("name")) { - preset.Name = xml.attributes().value("name").toString(); - } - if (xml.attributes().hasAttribute("url")) { + if (xml.attributes().hasAttribute("name")) { + preset.Name = xml.attributes().value("name").toString(); + } + if (xml.attributes().hasAttribute("url")) { preset.Url = xml.attributes().value("url").toString(); - } + } + qCDebug(dcBluOS()) << "Preset text" << xml.readElementText(); //apparently the text must be read so the xml parser recognises the next element presetList.append(preset); } else { xml.skipCurrentElement(); @@ -325,12 +327,13 @@ QUuid BluOS::loadPreset(int preset) url.setScheme("http"); url.setHost(m_hostAddress.toString()); url.setPort(m_port); - url.setPath("/Presets"); + url.setPath("/Preset"); QUrlQuery query; query.addQueryItem("id", QString::number(preset)); url.setQuery(query); + qCDebug(dcBluOS()) << "Loading preset" << url.toString(); QNetworkReply *reply = m_networkManager->get(QNetworkRequest(url)); - connect(reply, &QNetworkReply::finished, this, [reply, this] { + connect(reply, &QNetworkReply::finished, this, [requestId, reply, this] { reply->deleteLater(); int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); @@ -339,11 +342,12 @@ QUuid BluOS::loadPreset(int preset) 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; } @@ -358,7 +362,7 @@ QUuid BluOS::getSources() url.setPort(m_port); url.setPath("/Browse"); QNetworkReply *reply = m_networkManager->get(QNetworkRequest(url)); - connect(reply, &QNetworkReply::finished, this, [reply, this] { + connect(reply, &QNetworkReply::finished, this, [requestId, reply, this] { reply->deleteLater(); int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); @@ -367,11 +371,110 @@ QUuid BluOS::getSources() if (reply->error() == QNetworkReply::HostNotFoundError) { emit connectionChanged(false); } - qCWarning(dcBluOS()) << "Request error:" << status << reply->errorString(); return; } emit connectionChanged(true); + QByteArray data = reply->readAll(); + qCDebug(dcBluOS()) << "Sources: " << data; + QXmlStreamReader xml; + xml.addData(data); + if (xml.hasError()) { + qCDebug(dcBluOS()) << "XML Error:" << xml.errorString(); + return; + } + QList sourceList; + if (xml.readNextStartElement()) { + if (xml.name() == "browse") { + while(xml.readNextStartElement()){ + if(xml.name() == "item"){ + Source source; + if (xml.attributes().hasAttribute("text")) { + source.Text = xml.attributes().value("text").toString(); + } + if (xml.attributes().hasAttribute("type")) { + source.Type = xml.attributes().value("type").toString(); + } + if (xml.attributes().hasAttribute("browseKey")) { + source.BrowseKey = xml.attributes().value("browseKey").toString(); + } + if (xml.attributes().hasAttribute("image")) { + source.Image = xml.attributes().value("image").toString(); + } + qCDebug(dcBluOS()) << "Source text" << xml.readElementText(); + sourceList.append(source); + } else { + xml.skipCurrentElement(); + } + } + } + } + emit sourcesReceived(requestId, sourceList); + }); + return requestId; +} + +QUuid BluOS::browseSource(const QString &key) +{ + QUuid requestId = QUuid::createUuid(); + + QUrl url; + url.setScheme("http"); + url.setHost(m_hostAddress.toString()); + url.setPort(m_port); + url.setPath("/Browse"); + QUrlQuery query; + query.addQueryItem("key", key); + url.setQuery(query); + QNetworkReply *reply = m_networkManager->get(QNetworkRequest(url)); + connect(reply, &QNetworkReply::finished, 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); + } + qCWarning(dcBluOS()) << "Request error:" << status << reply->errorString(); + return; + } + emit connectionChanged(true); + QByteArray data = reply->readAll(); + qCDebug(dcBluOS()) << "Browse result: " << data; + QXmlStreamReader xml; + xml.addData(data); + if (xml.hasError()) { + qCDebug(dcBluOS()) << "XML Error:" << xml.errorString(); + return; + } + QList sourceList; + if (xml.readNextStartElement()) { + if (xml.name() == "browse") { + while(xml.readNextStartElement()){ + if(xml.name() == "item"){ + Source source; + if (xml.attributes().hasAttribute("text")) { + source.Text = xml.attributes().value("text").toString(); + } + if (xml.attributes().hasAttribute("type")) { + source.Type = xml.attributes().value("type").toString(); + } + if (xml.attributes().hasAttribute("browseKey")) { + source.BrowseKey = xml.attributes().value("browseKey").toString(); + } + if (xml.attributes().hasAttribute("image")) { + source.Image = xml.attributes().value("image").toString(); + } + qCDebug(dcBluOS()) << "Source text" << xml.readElementText(); + sourceList.append(source); + } else { + xml.skipCurrentElement(); + } + } + } + } + emit sourcesReceived(requestId, sourceList); }); return requestId; } diff --git a/bluos/bluos.h b/bluos/bluos.h index 0dacabcd..f929a0ab 100644 --- a/bluos/bluos.h +++ b/bluos/bluos.h @@ -122,6 +122,7 @@ public: // Content Browsing QUuid getSources(); + QUuid browseSource(const QString &key); // Player Grouping QUuid addGroupPlayer(QHostAddress address, int port); //adds player as slave @@ -144,5 +145,6 @@ signals: void presetsReceived(QUuid requestId, const QList &presets); void sourcesReceived(QUuid requestId, const QList &sources); + void browseResultReceived(QUuid requestId, const QList &sources); }; #endif // BLUOS_H diff --git a/bluos/integrationpluginbluos.cpp b/bluos/integrationpluginbluos.cpp index 6b229152..1d386136 100644 --- a/bluos/integrationpluginbluos.cpp +++ b/bluos/integrationpluginbluos.cpp @@ -232,20 +232,28 @@ void IntegrationPluginBluOS::browseThing(BrowseResult *result) QUuid requestId = bluos->listPresets(); m_asyncBrowseResults.insert(requestId, result); connect(result, &BrowseResult::aborted, this, [this, requestId]{m_asyncBrowseResults.remove(requestId);}); - } else { + } else if (result->itemId() == "grouping") { + //TODO avahi discovery + // m_asyncBrowseResults.insert(requestId, result); + //connect(result, &BrowseResult::aborted, this, [this, requestId]{m_asyncBrowseResults.remove(requestId);}); + } else if (result->itemId().isEmpty()) { MediaBrowserItem presetItem("presets", "Presets", true, false); presetItem.setIcon(BrowserItem::BrowserIcon::BrowserIconFavorites); presetItem.setMediaIcon(MediaBrowserItem::MediaBrowserIconMusicLibrary); result->addItem(presetItem); MediaBrowserItem groupingItem("grouping", "Grouping", true, false); - presetItem.setIcon(BrowserItem::BrowserIcon::BrowserIconApplication); - presetItem.setMediaIcon(MediaBrowserItem::MediaBrowserIconNetwork); - result->addItem(presetItem); + groupingItem.setIcon(BrowserItem::BrowserIcon::BrowserIconApplication); + groupingItem.setMediaIcon(MediaBrowserItem::MediaBrowserIconNetwork); + result->addItem(groupingItem); QUuid requestId = bluos->getSources(); m_asyncBrowseResults.insert(requestId, result); connect(result, &BrowseResult::aborted, this, [this, requestId]{m_asyncBrowseResults.remove(requestId);}); + } else { + QUuid requestId = bluos->browseSource(result->itemId()); + m_asyncBrowseResults.insert(requestId, result); + connect(result, &BrowseResult::aborted, this, [this, requestId]{m_asyncBrowseResults.remove(requestId);}); } } } @@ -274,7 +282,24 @@ void IntegrationPluginBluOS::browserItem(BrowserItemResult *result) void IntegrationPluginBluOS::executeBrowserItem(BrowserActionInfo *info) { - Q_UNUSED(info) + Thing *thing = info->thing(); + if (thing->thingClassId() == bluosPlayerThingClassId) { + BluOS *bluos = m_bluos.value(thing->id()); + if (!bluos) + return; + + if (info->browserAction().itemId().startsWith("presets")) { + QUuid requestId; + int presetId = info->browserAction().itemId().split("&").last().toInt(); + requestId = bluos->loadPreset(presetId); + m_asyncExecuteBrowseItems.insert(requestId, info); + connect(info, &BrowserActionInfo::aborted, this, [this, requestId]{m_asyncExecuteBrowseItems.remove(requestId);}); + } else if (info->browserAction().itemId().startsWith("grouping")) { + //TODO Grouping + } else { + //TODO Sources + } + } } void IntegrationPluginBluOS::onConnectionChanged(bool connected) @@ -351,6 +376,15 @@ void IntegrationPluginBluOS::onActionExecuted(QUuid requestId, bool success) info->finish(Thing::ThingErrorHardwareNotAvailable); } } + if (m_asyncExecuteBrowseItems.contains(requestId)) { + BrowserActionInfo *info = m_asyncExecuteBrowseItems.take(requestId); + if (success) { + info->finish(Thing::ThingErrorNoError); + } else { + info->finish(Thing::ThingErrorHardwareFailure); + } + m_pluginTimer->timeout(); // get a status update + } } void IntegrationPluginBluOS::onVolumeReceived(int volume, bool mute) @@ -369,10 +403,10 @@ void IntegrationPluginBluOS::onPresetsReceived(QUuid requestId, const QListaddItem(item); @@ -394,9 +428,63 @@ void IntegrationPluginBluOS::onSourcesReceived(QUuid requestId, const QListaddItem(item); + } + result->finish(Thing::ThingErrorNoError); + } + if (m_asyncBrowseItemResults.contains(requestId)) { + BrowserItemResult *result = m_asyncBrowseItemResults.take(requestId); + Q_UNUSED(result) + } +} + +void IntegrationPluginBluOS::onBrowseResultReceived(QUuid requestId, const QList &sources) +{ + BluOS *bluos = static_cast(sender()); + Thing *thing = myThings().findById(m_bluos.key(bluos)); + if (!thing) + return; + if (m_asyncBrowseResults.contains(requestId)) { + BrowseResult *result = m_asyncBrowseResults.take(requestId); + foreach(BluOS::Source source, sources) { + qCDebug(dcBluOS()) << "Source added" << source.Text << source.BrowseKey << source.Type; + MediaBrowserItem item; + item.setDisplayName(source.Text); + if (source.BrowseKey.isEmpty()) { + item.setBrowsable(false); + item.setExecutable(true); + item.setId(source.Text); + } else { + item.setBrowsable(true); + item.setExecutable(false); + item.setId(source.BrowseKey); + } + item.setIcon(BrowserItem::BrowserIconMusic); result->addItem(item); } result->finish(Thing::ThingErrorNoError); diff --git a/bluos/integrationpluginbluos.h b/bluos/integrationpluginbluos.h index b027d7d1..b61dce7c 100644 --- a/bluos/integrationpluginbluos.h +++ b/bluos/integrationpluginbluos.h @@ -86,6 +86,6 @@ private slots: void onPresetsReceived(QUuid requestId, const QList &presets); void onSourcesReceived(QUuid requestId, const QList &sources); + void onBrowseResultReceived(QUuid requestId, const QList &sources); }; - #endif // INTEGRATIONPLUGINBLUOS_H From e95ff04808f13cb93b01be0a56f10bfb50ba087b Mon Sep 17 00:00:00 2001 From: "bernhard.trinnes" Date: Tue, 17 Mar 2020 12:08:24 +0100 Subject: [PATCH 06/14] fixed shuffle and repeat state --- bluos/bluos.cpp | 30 ++++++++++++++++++++ bluos/bluos.h | 3 ++ bluos/integrationpluginbluos.cpp | 47 ++++++++++++++++++++++++------- bluos/integrationpluginbluos.h | 3 +- bluos/integrationpluginbluos.json | 8 ++++++ 5 files changed, 80 insertions(+), 11 deletions(-) diff --git a/bluos/bluos.cpp b/bluos/bluos.cpp index 64875f27..69bd7e83 100644 --- a/bluos/bluos.cpp +++ b/bluos/bluos.cpp @@ -220,6 +220,19 @@ QUuid BluOS::setShuffle(bool shuffle) } emit connectionChanged(true); emit actionExecuted(requestId, true); + + QXmlStreamReader xml; + xml.addData(reply->readAll()); + if (xml.hasError()) { + qCDebug(dcBluOS()) << "XML Error:" << xml.errorString(); + return; + } + if (xml.readNextStartElement()) { + if (xml.attributes().hasAttribute("shuffle")) { + bool shuffle = RepeatMode(xml.attributes().value("shuffle").toInt()); + emit shuffleStateReceived(shuffle); + } + } }); return requestId; } @@ -254,6 +267,21 @@ QUuid BluOS::setRepeat(RepeatMode repeatMode) } emit connectionChanged(true); emit actionExecuted(requestId, true); + + QXmlStreamReader xml; + xml.addData(reply->readAll()); + if (xml.hasError()) { + qCDebug(dcBluOS()) << "XML Error:" << xml.errorString(); + return; + } + if (xml.readNextStartElement()) { + if (xml.name() == "playlist") { + if (xml.attributes().hasAttribute("repeat")) { + RepeatMode mode = RepeatMode(xml.attributes().value("repeat").toInt()); + emit repeatModeReceived(mode); + } + } + } }); return requestId; } @@ -652,6 +680,8 @@ bool BluOS::parseState(const QByteArray &state) statusResponse.Image = xml.readElementText(); } else if(xml.name() == "title1") { statusResponse.Title = xml.readElementText(); + } else if(xml.name() == "group") { + statusResponse.Group = xml.readElementText(); } else { xml.skipCurrentElement(); } diff --git a/bluos/bluos.h b/bluos/bluos.h index f929a0ab..65771f67 100644 --- a/bluos/bluos.h +++ b/bluos/bluos.h @@ -80,6 +80,7 @@ public: RepeatMode Repeat; bool Shuffle; QUrl Image; + QString Group; }; struct Preset { @@ -142,6 +143,8 @@ signals: void statusReceived(const StatusResponse &status); void volumeReceived(int volume, bool mute); + void shuffleStateReceived(bool state); + void repeatModeReceived(RepeatMode mode); void presetsReceived(QUuid requestId, const QList &presets); void sourcesReceived(QUuid requestId, const QList &sources); diff --git a/bluos/integrationpluginbluos.cpp b/bluos/integrationpluginbluos.cpp index 1d386136..043a6948 100644 --- a/bluos/integrationpluginbluos.cpp +++ b/bluos/integrationpluginbluos.cpp @@ -47,11 +47,6 @@ IntegrationPluginBluOS::IntegrationPluginBluOS() } -IntegrationPluginBluOS::~IntegrationPluginBluOS() -{ - -} - void IntegrationPluginBluOS::init() { m_serviceBrowser = hardwareManager()->zeroConfController()->createServiceBrowser("_musc._tcp"); @@ -99,13 +94,14 @@ void IntegrationPluginBluOS::setupThing(ThingSetupInfo *info) connect(bluos, &BluOS::volumeReceived, this, &IntegrationPluginBluOS::onVolumeReceived); connect(bluos, &BluOS::presetsReceived, this, &IntegrationPluginBluOS::onPresetsReceived); connect(bluos, &BluOS::sourcesReceived, this, &IntegrationPluginBluOS::onSourcesReceived); + connect(bluos, &BluOS::shuffleStateReceived, this, &IntegrationPluginBluOS::onShuffleStateReceived); + connect(bluos, &BluOS::repeatModeReceived, this, &IntegrationPluginBluOS::onRepeatModeReceived); m_asyncSetup.insert(bluos, info); bluos->getStatus(); // In case the setup is cancelled before we finish it... connect(info, &QObject::destroyed, this, [this, bluos]() { m_asyncSetup.remove(bluos); - }); return; } else { @@ -242,10 +238,10 @@ void IntegrationPluginBluOS::browseThing(BrowseResult *result) presetItem.setMediaIcon(MediaBrowserItem::MediaBrowserIconMusicLibrary); result->addItem(presetItem); - MediaBrowserItem groupingItem("grouping", "Grouping", true, false); - groupingItem.setIcon(BrowserItem::BrowserIcon::BrowserIconApplication); - groupingItem.setMediaIcon(MediaBrowserItem::MediaBrowserIconNetwork); - result->addItem(groupingItem); + // MediaBrowserItem groupingItem("grouping", "Grouping", true, false); + // groupingItem.setIcon(BrowserItem::BrowserIcon::BrowserIconApplication); + // groupingItem.setMediaIcon(MediaBrowserItem::MediaBrowserIconNetwork); + // result->addItem(groupingItem); QUuid requestId = bluos->getSources(); m_asyncBrowseResults.insert(requestId, result); @@ -364,6 +360,7 @@ void IntegrationPluginBluOS::onStatusResponseReceived(const BluOS::StatusRespons thing->setStateValue(bluosPlayerRepeatStateTypeId, "None"); break; } + thing->setStateValue(bluosPlayerGroupStateTypeId, status.Group); } void IntegrationPluginBluOS::onActionExecuted(QUuid requestId, bool success) @@ -397,6 +394,35 @@ void IntegrationPluginBluOS::onVolumeReceived(int volume, bool mute) thing->setStateValue(bluosPlayerVolumeStateTypeId, volume); } +void IntegrationPluginBluOS::onShuffleStateReceived(bool state) +{ + BluOS *bluos = static_cast(sender()); + Thing *thing = myThings().findById(m_bluos.key(bluos)); + if (!thing) + return; + thing->setStateValue(bluosPlayerShuffleStateTypeId, state); +} + +void IntegrationPluginBluOS::onRepeatModeReceived(BluOS::RepeatMode mode) +{ + BluOS *bluos = static_cast(sender()); + Thing *thing = myThings().findById(m_bluos.key(bluos)); + if (!thing) + return; + switch (mode) { + case BluOS::RepeatMode::All: + thing->setStateValue(bluosPlayerRepeatStateTypeId, "All"); + break; + case BluOS::RepeatMode::One: + thing->setStateValue(bluosPlayerRepeatStateTypeId, "One"); + break; + case BluOS::RepeatMode::None: + thing->setStateValue(bluosPlayerRepeatStateTypeId, "None"); + break; + } + +} + void IntegrationPluginBluOS::onPresetsReceived(QUuid requestId, const QList &presets) { BluOS *bluos = static_cast(sender()); @@ -444,6 +470,7 @@ void IntegrationPluginBluOS::onSourcesReceived(QUuid requestId, const QList &presets); void onSourcesReceived(QUuid requestId, const QList &sources); diff --git a/bluos/integrationpluginbluos.json b/bluos/integrationpluginbluos.json index 33e94154..3d946475 100644 --- a/bluos/integrationpluginbluos.json +++ b/bluos/integrationpluginbluos.json @@ -142,6 +142,14 @@ "displayNameEvent": "Artwork changed", "type": "QString", "defaultValue": "" + }, + { + "id": "69757ef3-173f-499c-9304-4252de3588c6", + "name": "group", + "displayName": "Group", + "displayNameEvent": "Group changed", + "type": "QString", + "defaultValue": "" } ], "actionTypes": [ From 00a417a953b9c6ca5a6fcccd4d8a72e9c19614d7 Mon Sep 17 00:00:00 2001 From: "bernhard.trinnes" Date: Fri, 20 Mar 2020 07:38:12 +0100 Subject: [PATCH 07/14] added speaker grouping --- bluos/README.md | 26 ++++++++++++++++++++++++++ bluos/integrationpluginbluos.cpp | 16 +++++++++++++--- 2 files changed, 39 insertions(+), 3 deletions(-) diff --git a/bluos/README.md b/bluos/README.md index e69de29b..68f37ec0 100644 --- a/bluos/README.md +++ b/bluos/README.md @@ -0,0 +1,26 @@ +# BluOS + +This integration allows to control audio devices based on BluOS. BluOS is an operating system that can be found in products from Blusound, NAD Electronics, DALI Loudspeakers and others. + +## Supported Things + +* BluOS Player + * Auto discovery setup + * Auto rediscovery on IP address change + * Multimedia control + * Volume + * Browsing Presets + * Browsing various music services + * Grouping speakers + * No internet or cloud connection required + +## Requirements + +* The BluOS device must be in the same local area network as nymea. +* TCP sockets on port 80 must not be blocked by the router. +* Blusound App to setup the speaker. +* The package "nymea-plugin-bluos" must be installed + +## More + +https://www.bluesound.com/ diff --git a/bluos/integrationpluginbluos.cpp b/bluos/integrationpluginbluos.cpp index 043a6948..d8669c81 100644 --- a/bluos/integrationpluginbluos.cpp +++ b/bluos/integrationpluginbluos.cpp @@ -229,9 +229,19 @@ void IntegrationPluginBluOS::browseThing(BrowseResult *result) m_asyncBrowseResults.insert(requestId, result); connect(result, &BrowseResult::aborted, this, [this, requestId]{m_asyncBrowseResults.remove(requestId);}); } else if (result->itemId() == "grouping") { - //TODO avahi discovery - // m_asyncBrowseResults.insert(requestId, result); - //connect(result, &BrowseResult::aborted, this, [this, requestId]{m_asyncBrowseResults.remove(requestId);}); + foreach (const ZeroConfServiceEntry avahiEntry, m_serviceBrowser->serviceEntries()) { + qCDebug(dcBluOS()) << "Zeroconf entry:" << avahiEntry; + + QString playerId = avahiEntry.hostName().split(".").first(); + if (thing->paramValue(bluosPlayerThingSerialNumberParamTypeId).toString() == playerId) { + continue; + } + MediaBrowserItem groupingItem("grouping&"+avahiEntry.hostAddress().toString()+"&"+avahiEntry.port(), avahiEntry.name(), true, false); + groupingItem.setDescription(avahiEntry.hostAddress().toString()); + groupingItem.setMediaIcon(MediaBrowserItem::MediaBrowserIconNetwork); + groupingItem.setIcon(BrowserItem::BrowserIconMusic); + result->addItem(groupingItem); + } } else if (result->itemId().isEmpty()) { MediaBrowserItem presetItem("presets", "Presets", true, false); presetItem.setIcon(BrowserItem::BrowserIcon::BrowserIconFavorites); From b2c1d7693cdc00dcf0f41a8cbc965b9372e06233 Mon Sep 17 00:00:00 2001 From: "bernhard.trinnes" Date: Thu, 26 Mar 2020 19:52:56 +0100 Subject: [PATCH 08/14] removed untested services from browsing --- bluos/integrationpluginbluos.cpp | 9 +++++++-- debian/nymea-plugin-bluos.install.in | 2 +- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/bluos/integrationpluginbluos.cpp b/bluos/integrationpluginbluos.cpp index d8669c81..ec1000b2 100644 --- a/bluos/integrationpluginbluos.cpp +++ b/bluos/integrationpluginbluos.cpp @@ -479,18 +479,23 @@ void IntegrationPluginBluOS::onSourcesReceived(QUuid requestId, const QListaddItem(item); } else if (source.Text == "Spotify") { item.setExecutable(false); + item.setBrowsable(false); item.setMediaIcon(MediaBrowserItem::MediaBrowserIconSpotify); item.setDescription("Open the Spotify App for browsing"); + result->addItem(item); } else if (source.Text == "TuneIn") { item.setMediaIcon(MediaBrowserItem::MediaBrowserIconTuneIn); + result->addItem(item); } else if (source.Text.contains("Aux")) { item.setMediaIcon(MediaBrowserItem::MediaBrowserIconAux); + result->addItem(item); } else if (source.Text == "Radio Paradise") { - //item.setMediaIcon(MediaBrowserItem::MediaBrowserIconRadioParadise); + item.setMediaIcon(MediaBrowserItem::MediaBrowserIconRadioParadise); + //result->addItem(item); } - result->addItem(item); } result->finish(Thing::ThingErrorNoError); } diff --git a/debian/nymea-plugin-bluos.install.in b/debian/nymea-plugin-bluos.install.in index 11c5b050..f0a17ed6 100644 --- a/debian/nymea-plugin-bluos.install.in +++ b/debian/nymea-plugin-bluos.install.in @@ -1 +1 @@ -usr/lib/@DEB_HOST_MULTIARCH@/nymea/plugins/libnymea_devicepluginbluos.so +usr/lib/@DEB_HOST_MULTIARCH@/nymea/plugins/libnymea_integrationpluginbluos.so From 5e6ac9bd15a663b00dafd129f1384cb880d1c080 Mon Sep 17 00:00:00 2001 From: "bernhard.trinnes" Date: Mon, 6 Apr 2020 22:12:12 +0200 Subject: [PATCH 09/14] added translation files --- ...d25b3-37ef-4b27-abca-24989fa38c61-de_DE.ts | 295 ++++++++++++++++++ ...d25b3-37ef-4b27-abca-24989fa38c61-en_US.ts | 295 ++++++++++++++++++ 2 files changed, 590 insertions(+) create mode 100644 bluos/translations/71dd25b3-37ef-4b27-abca-24989fa38c61-de_DE.ts create mode 100644 bluos/translations/71dd25b3-37ef-4b27-abca-24989fa38c61-en_US.ts diff --git a/bluos/translations/71dd25b3-37ef-4b27-abca-24989fa38c61-de_DE.ts b/bluos/translations/71dd25b3-37ef-4b27-abca-24989fa38c61-de_DE.ts new file mode 100644 index 00000000..91a9f642 --- /dev/null +++ b/bluos/translations/71dd25b3-37ef-4b27-abca-24989fa38c61-de_DE.ts @@ -0,0 +1,295 @@ + + + + + BluOS + + + + Album + The name of the ParamType (ThingClass: bluosPlayer, EventType: collection, ID: {d2a2db24-5855-40cd-a043-6f67e43acc61}) +---------- +The name of the StateType ({d2a2db24-5855-40cd-a043-6f67e43acc61}) of ThingClass bluosPlayer + Album + + + + Album changed + The name of the EventType ({d2a2db24-5855-40cd-a043-6f67e43acc61}) of ThingClass bluosPlayer + Album geändert + + + + + Artist + The name of the ParamType (ThingClass: bluosPlayer, EventType: artist, ID: {8c5372f1-3dde-4984-9955-9a436a29e8e3}) +---------- +The name of the StateType ({8c5372f1-3dde-4984-9955-9a436a29e8e3}) of ThingClass bluosPlayer + Künstler + + + + Artist changed + The name of the EventType ({8c5372f1-3dde-4984-9955-9a436a29e8e3}) of ThingClass bluosPlayer + Künstler geändert + + + + + Artwork + The name of the ParamType (ThingClass: bluosPlayer, EventType: artwork, ID: {918bfeed-bf96-4034-85d6-9f4cff35fd00}) +---------- +The name of the StateType ({918bfeed-bf96-4034-85d6-9f4cff35fd00}) of ThingClass bluosPlayer + Artwork + + + + Artwork changed + The name of the EventType ({918bfeed-bf96-4034-85d6-9f4cff35fd00}) of ThingClass bluosPlayer + Artwork geändert + + + + + BluOS + The name of the vendor ({39a492e9-e497-4b43-94d4-970eb9913b96}) +---------- +The name of the plugin BluOS ({71dd25b3-37ef-4b27-abca-24989fa38c61}) + BluOS + + + + BluOS player + The name of the ThingClass ({406adcbc-1e7d-4e41-ae2a-f87b6bafd13d}) + BluOS Player + + + + + Connected + The name of the ParamType (ThingClass: bluosPlayer, EventType: connected, ID: {8092f387-5099-43e8-a7c4-7f0dd7b70fce}) +---------- +The name of the StateType ({8092f387-5099-43e8-a7c4-7f0dd7b70fce}) of ThingClass bluosPlayer + Verbunden + + + + Connected changed + The name of the EventType ({8092f387-5099-43e8-a7c4-7f0dd7b70fce}) of ThingClass bluosPlayer + Verbunden geändert + + + + + Group + The name of the ParamType (ThingClass: bluosPlayer, EventType: group, ID: {69757ef3-173f-499c-9304-4252de3588c6}) +---------- +The name of the StateType ({69757ef3-173f-499c-9304-4252de3588c6}) of ThingClass bluosPlayer + Gruppe + + + + Group changed + The name of the EventType ({69757ef3-173f-499c-9304-4252de3588c6}) of ThingClass bluosPlayer + Gruppe geändert + + + + IP Address + The name of the ParamType (ThingClass: bluosPlayer, Type: thing, ID: {833f99cc-fc3f-48ef-a705-a69ae2c8e9ec}) + IP-Adresse + + + + + + Mute + The name of the ParamType (ThingClass: bluosPlayer, ActionType: mute, ID: {2f650ae6-f3a5-4851-8449-1ba02c4864e5}) +---------- +The name of the ParamType (ThingClass: bluosPlayer, EventType: mute, ID: {2f650ae6-f3a5-4851-8449-1ba02c4864e5}) +---------- +The name of the StateType ({2f650ae6-f3a5-4851-8449-1ba02c4864e5}) of ThingClass bluosPlayer + Stumm + + + + Mute changed + The name of the EventType ({2f650ae6-f3a5-4851-8449-1ba02c4864e5}) of ThingClass bluosPlayer + Stumm geändert + + + + Pause + The name of the ActionType ({92446566-8c32-4ee2-9498-d9dd9333a75d}) of ThingClass bluosPlayer + Pause + + + + Play + The name of the ActionType ({253eb62f-d50d-4667-8213-8632de178aa3}) of ThingClass bluosPlayer + Play + + + + + + Playback status + The name of the ParamType (ThingClass: bluosPlayer, ActionType: playbackStatus, ID: {43f552c7-7dfe-4358-bebd-ab558191cdfc}) +---------- +The name of the ParamType (ThingClass: bluosPlayer, EventType: playbackStatus, ID: {43f552c7-7dfe-4358-bebd-ab558191cdfc}) +---------- +The name of the StateType ({43f552c7-7dfe-4358-bebd-ab558191cdfc}) of ThingClass bluosPlayer + Playbackstatus + + + + Playback status changed + The name of the EventType ({43f552c7-7dfe-4358-bebd-ab558191cdfc}) of ThingClass bluosPlayer + Playbackstatus geändert + + + + Port + The name of the ParamType (ThingClass: bluosPlayer, Type: thing, ID: {4628c040-6bbb-43c9-b25f-ce3b22300e3b}) + Port + + + + + + Repeat mode + The name of the ParamType (ThingClass: bluosPlayer, ActionType: repeat, ID: {b4a572b1-6120-43e8-9a6c-18d099c8b162}) +---------- +The name of the ParamType (ThingClass: bluosPlayer, EventType: repeat, ID: {b4a572b1-6120-43e8-9a6c-18d099c8b162}) +---------- +The name of the StateType ({b4a572b1-6120-43e8-9a6c-18d099c8b162}) of ThingClass bluosPlayer + Wiederholungsmodus + + + + Repeat mode changed + The name of the EventType ({b4a572b1-6120-43e8-9a6c-18d099c8b162}) of ThingClass bluosPlayer + Wiederholungsmodus geändert + + + + Serial number + The name of the ParamType (ThingClass: bluosPlayer, Type: thing, ID: {8fd2a7d5-bb26-4f18-a488-5ab0779733f8}) + Seriennummer + + + + Set mute + The name of the ActionType ({2f650ae6-f3a5-4851-8449-1ba02c4864e5}) of ThingClass bluosPlayer + Setze stumm + + + + Set playback status + The name of the ActionType ({43f552c7-7dfe-4358-bebd-ab558191cdfc}) of ThingClass bluosPlayer + Setze Playbackstatus + + + + Set repeat mode + The name of the ActionType ({b4a572b1-6120-43e8-9a6c-18d099c8b162}) of ThingClass bluosPlayer + Setze Wiederholungsmodus + + + + Set shuffle + The name of the ActionType ({5636c21a-f14b-472c-8486-177543f6adfb}) of ThingClass bluosPlayer + Setze Zufallswiedergabe + + + + Set volume + The name of the ActionType ({a444bb7c-7266-4c7e-874a-eb04bb91b9cf}) of ThingClass bluosPlayer + Setze Lautstärke + + + + + + Shuffle + The name of the ParamType (ThingClass: bluosPlayer, ActionType: shuffle, ID: {5636c21a-f14b-472c-8486-177543f6adfb}) +---------- +The name of the ParamType (ThingClass: bluosPlayer, EventType: shuffle, ID: {5636c21a-f14b-472c-8486-177543f6adfb}) +---------- +The name of the StateType ({5636c21a-f14b-472c-8486-177543f6adfb}) of ThingClass bluosPlayer + Zufallswiedergabe + + + + Shuffle changed + The name of the EventType ({5636c21a-f14b-472c-8486-177543f6adfb}) of ThingClass bluosPlayer + Zufallswiedergabe geändert + + + + Skip back + The name of the ActionType ({864a5bcc-71e1-4d7f-8f2e-3c4222d5d988}) of ThingClass bluosPlayer + Vorheriges + + + + Skip next + The name of the ActionType ({30930095-6f97-48ef-8bbf-597d734f0751}) of ThingClass bluosPlayer + Nächstes + + + + + Source + The name of the ParamType (ThingClass: bluosPlayer, EventType: source, ID: {604e4995-ee1a-44fc-bfcb-ca0e861710bd}) +---------- +The name of the StateType ({604e4995-ee1a-44fc-bfcb-ca0e861710bd}) of ThingClass bluosPlayer + Quelle + + + + Source changed + The name of the EventType ({604e4995-ee1a-44fc-bfcb-ca0e861710bd}) of ThingClass bluosPlayer + Quelle geändert + + + + Stop + The name of the ActionType ({2526ec6d-1c21-4f73-97f1-973a5f05b626}) of ThingClass bluosPlayer + Stopp + + + + + Title + The name of the ParamType (ThingClass: bluosPlayer, EventType: title, ID: {6204ec9d-6aab-4fd3-8911-fdaa462c19a8}) +---------- +The name of the StateType ({6204ec9d-6aab-4fd3-8911-fdaa462c19a8}) of ThingClass bluosPlayer + Titel + + + + Title changed + The name of the EventType ({6204ec9d-6aab-4fd3-8911-fdaa462c19a8}) of ThingClass bluosPlayer + Titel geändert + + + + + + Volume + The name of the ParamType (ThingClass: bluosPlayer, ActionType: volume, ID: {a444bb7c-7266-4c7e-874a-eb04bb91b9cf}) +---------- +The name of the ParamType (ThingClass: bluosPlayer, EventType: volume, ID: {a444bb7c-7266-4c7e-874a-eb04bb91b9cf}) +---------- +The name of the StateType ({a444bb7c-7266-4c7e-874a-eb04bb91b9cf}) of ThingClass bluosPlayer + Lautstärke + + + + Volume changed + The name of the EventType ({a444bb7c-7266-4c7e-874a-eb04bb91b9cf}) of ThingClass bluosPlayer + Lautstärke geändert + + + diff --git a/bluos/translations/71dd25b3-37ef-4b27-abca-24989fa38c61-en_US.ts b/bluos/translations/71dd25b3-37ef-4b27-abca-24989fa38c61-en_US.ts new file mode 100644 index 00000000..2f97198a --- /dev/null +++ b/bluos/translations/71dd25b3-37ef-4b27-abca-24989fa38c61-en_US.ts @@ -0,0 +1,295 @@ + + + + + BluOS + + + + Album + The name of the ParamType (ThingClass: bluosPlayer, EventType: collection, ID: {d2a2db24-5855-40cd-a043-6f67e43acc61}) +---------- +The name of the StateType ({d2a2db24-5855-40cd-a043-6f67e43acc61}) of ThingClass bluosPlayer + + + + + Album changed + The name of the EventType ({d2a2db24-5855-40cd-a043-6f67e43acc61}) of ThingClass bluosPlayer + + + + + + Artist + The name of the ParamType (ThingClass: bluosPlayer, EventType: artist, ID: {8c5372f1-3dde-4984-9955-9a436a29e8e3}) +---------- +The name of the StateType ({8c5372f1-3dde-4984-9955-9a436a29e8e3}) of ThingClass bluosPlayer + + + + + Artist changed + The name of the EventType ({8c5372f1-3dde-4984-9955-9a436a29e8e3}) of ThingClass bluosPlayer + + + + + + Artwork + The name of the ParamType (ThingClass: bluosPlayer, EventType: artwork, ID: {918bfeed-bf96-4034-85d6-9f4cff35fd00}) +---------- +The name of the StateType ({918bfeed-bf96-4034-85d6-9f4cff35fd00}) of ThingClass bluosPlayer + + + + + Artwork changed + The name of the EventType ({918bfeed-bf96-4034-85d6-9f4cff35fd00}) of ThingClass bluosPlayer + + + + + + BluOS + The name of the vendor ({39a492e9-e497-4b43-94d4-970eb9913b96}) +---------- +The name of the plugin BluOS ({71dd25b3-37ef-4b27-abca-24989fa38c61}) + + + + + BluOS player + The name of the ThingClass ({406adcbc-1e7d-4e41-ae2a-f87b6bafd13d}) + + + + + + Connected + The name of the ParamType (ThingClass: bluosPlayer, EventType: connected, ID: {8092f387-5099-43e8-a7c4-7f0dd7b70fce}) +---------- +The name of the StateType ({8092f387-5099-43e8-a7c4-7f0dd7b70fce}) of ThingClass bluosPlayer + + + + + Connected changed + The name of the EventType ({8092f387-5099-43e8-a7c4-7f0dd7b70fce}) of ThingClass bluosPlayer + + + + + + Group + The name of the ParamType (ThingClass: bluosPlayer, EventType: group, ID: {69757ef3-173f-499c-9304-4252de3588c6}) +---------- +The name of the StateType ({69757ef3-173f-499c-9304-4252de3588c6}) of ThingClass bluosPlayer + + + + + Group changed + The name of the EventType ({69757ef3-173f-499c-9304-4252de3588c6}) of ThingClass bluosPlayer + + + + + IP Address + The name of the ParamType (ThingClass: bluosPlayer, Type: thing, ID: {833f99cc-fc3f-48ef-a705-a69ae2c8e9ec}) + + + + + + + Mute + The name of the ParamType (ThingClass: bluosPlayer, ActionType: mute, ID: {2f650ae6-f3a5-4851-8449-1ba02c4864e5}) +---------- +The name of the ParamType (ThingClass: bluosPlayer, EventType: mute, ID: {2f650ae6-f3a5-4851-8449-1ba02c4864e5}) +---------- +The name of the StateType ({2f650ae6-f3a5-4851-8449-1ba02c4864e5}) of ThingClass bluosPlayer + + + + + Mute changed + The name of the EventType ({2f650ae6-f3a5-4851-8449-1ba02c4864e5}) of ThingClass bluosPlayer + + + + + Pause + The name of the ActionType ({92446566-8c32-4ee2-9498-d9dd9333a75d}) of ThingClass bluosPlayer + + + + + Play + The name of the ActionType ({253eb62f-d50d-4667-8213-8632de178aa3}) of ThingClass bluosPlayer + + + + + + + Playback status + The name of the ParamType (ThingClass: bluosPlayer, ActionType: playbackStatus, ID: {43f552c7-7dfe-4358-bebd-ab558191cdfc}) +---------- +The name of the ParamType (ThingClass: bluosPlayer, EventType: playbackStatus, ID: {43f552c7-7dfe-4358-bebd-ab558191cdfc}) +---------- +The name of the StateType ({43f552c7-7dfe-4358-bebd-ab558191cdfc}) of ThingClass bluosPlayer + + + + + Playback status changed + The name of the EventType ({43f552c7-7dfe-4358-bebd-ab558191cdfc}) of ThingClass bluosPlayer + + + + + Port + The name of the ParamType (ThingClass: bluosPlayer, Type: thing, ID: {4628c040-6bbb-43c9-b25f-ce3b22300e3b}) + + + + + + + Repeat mode + The name of the ParamType (ThingClass: bluosPlayer, ActionType: repeat, ID: {b4a572b1-6120-43e8-9a6c-18d099c8b162}) +---------- +The name of the ParamType (ThingClass: bluosPlayer, EventType: repeat, ID: {b4a572b1-6120-43e8-9a6c-18d099c8b162}) +---------- +The name of the StateType ({b4a572b1-6120-43e8-9a6c-18d099c8b162}) of ThingClass bluosPlayer + + + + + Repeat mode changed + The name of the EventType ({b4a572b1-6120-43e8-9a6c-18d099c8b162}) of ThingClass bluosPlayer + + + + + Serial number + The name of the ParamType (ThingClass: bluosPlayer, Type: thing, ID: {8fd2a7d5-bb26-4f18-a488-5ab0779733f8}) + + + + + Set mute + The name of the ActionType ({2f650ae6-f3a5-4851-8449-1ba02c4864e5}) of ThingClass bluosPlayer + + + + + Set playback status + The name of the ActionType ({43f552c7-7dfe-4358-bebd-ab558191cdfc}) of ThingClass bluosPlayer + + + + + Set repeat mode + The name of the ActionType ({b4a572b1-6120-43e8-9a6c-18d099c8b162}) of ThingClass bluosPlayer + + + + + Set shuffle + The name of the ActionType ({5636c21a-f14b-472c-8486-177543f6adfb}) of ThingClass bluosPlayer + + + + + Set volume + The name of the ActionType ({a444bb7c-7266-4c7e-874a-eb04bb91b9cf}) of ThingClass bluosPlayer + + + + + + + Shuffle + The name of the ParamType (ThingClass: bluosPlayer, ActionType: shuffle, ID: {5636c21a-f14b-472c-8486-177543f6adfb}) +---------- +The name of the ParamType (ThingClass: bluosPlayer, EventType: shuffle, ID: {5636c21a-f14b-472c-8486-177543f6adfb}) +---------- +The name of the StateType ({5636c21a-f14b-472c-8486-177543f6adfb}) of ThingClass bluosPlayer + + + + + Shuffle changed + The name of the EventType ({5636c21a-f14b-472c-8486-177543f6adfb}) of ThingClass bluosPlayer + + + + + Skip back + The name of the ActionType ({864a5bcc-71e1-4d7f-8f2e-3c4222d5d988}) of ThingClass bluosPlayer + + + + + Skip next + The name of the ActionType ({30930095-6f97-48ef-8bbf-597d734f0751}) of ThingClass bluosPlayer + + + + + + Source + The name of the ParamType (ThingClass: bluosPlayer, EventType: source, ID: {604e4995-ee1a-44fc-bfcb-ca0e861710bd}) +---------- +The name of the StateType ({604e4995-ee1a-44fc-bfcb-ca0e861710bd}) of ThingClass bluosPlayer + + + + + Source changed + The name of the EventType ({604e4995-ee1a-44fc-bfcb-ca0e861710bd}) of ThingClass bluosPlayer + + + + + Stop + The name of the ActionType ({2526ec6d-1c21-4f73-97f1-973a5f05b626}) of ThingClass bluosPlayer + + + + + + Title + The name of the ParamType (ThingClass: bluosPlayer, EventType: title, ID: {6204ec9d-6aab-4fd3-8911-fdaa462c19a8}) +---------- +The name of the StateType ({6204ec9d-6aab-4fd3-8911-fdaa462c19a8}) of ThingClass bluosPlayer + + + + + Title changed + The name of the EventType ({6204ec9d-6aab-4fd3-8911-fdaa462c19a8}) of ThingClass bluosPlayer + + + + + + + Volume + The name of the ParamType (ThingClass: bluosPlayer, ActionType: volume, ID: {a444bb7c-7266-4c7e-874a-eb04bb91b9cf}) +---------- +The name of the ParamType (ThingClass: bluosPlayer, EventType: volume, ID: {a444bb7c-7266-4c7e-874a-eb04bb91b9cf}) +---------- +The name of the StateType ({a444bb7c-7266-4c7e-874a-eb04bb91b9cf}) of ThingClass bluosPlayer + + + + + Volume changed + The name of the EventType ({a444bb7c-7266-4c7e-874a-eb04bb91b9cf}) of ThingClass bluosPlayer + + + + From 54f6e2c33f75b39de4668a5f4e850a4b1b797972 Mon Sep 17 00:00:00 2001 From: "bernhard.trinnes" Date: Wed, 27 May 2020 15:39:34 +0200 Subject: [PATCH 10/14] added meta file and logo --- bluos/blusound.png | Bin 0 -> 2416 bytes bluos/meta.json | 13 +++++++++++++ 2 files changed, 13 insertions(+) create mode 100644 bluos/blusound.png create mode 100644 bluos/meta.json diff --git a/bluos/blusound.png b/bluos/blusound.png new file mode 100644 index 0000000000000000000000000000000000000000..5477d89416bf514f5190a58c403c97aabc8b593d GIT binary patch literal 2416 zcmV-$36J)PP)Kzvu`1trKC@AFQ zYj>hB5QbrhiqRSbZ&6UxR@-Xz|NpxMYZCC%T1zHfPo7UbyL&dgCCOwm7aYf8u~;k? zi^XEGSS%Kc#bU8oEEbE!VzF2(7K_DVvHXVNd*_q)k9rkMN5etbFs-V{JNQSSGRP!J z-}j+uX-7J~TRDux9Kc;a?R%!>IeHZ4cpizV-%dxS1?p1h-m$RQH8Wy+5g!KH_HTol9TBg^Lk}t+>Q=~pX?VN#4JMy%zlKYiU+v? zsW7NoVaDh!F26_*X4Q}K4O$gCPPp(WD=75fO~)`_p;=+BG2lhRf&Lb?D|DQ30$#y* zz(UtRmqAdNIicHv@$g`&Z=?&LD0IdXsuX&Fdc46===5GeDI^5*AdaNaaTcJ~B7fF9 zRtYqPP7~xrWuSYo0`F)F^S3C?28?x&Cg}!f3Y}C*jPd4am*OexCCc^#+94cMp%bz25>IH zlUUvnA$e^RVK6Gcg}%&2-7g?nsD(Hwa-_@CxjOI@6pJxcHW+7J$N^n1CKxM2*UlASXWA39h9NBrXDAFtej5gF`hTx)!Ukjc7fu%n;YX$@41hPRCznwT;A96Tf09bO!yi zz&j)N5pf#iS#2-VtCL-vYVgj&LZVw8ETRyvyO)j6K&(SD!BgmjbEr6_CbK<0-E}Uc z3JoH0_Ud!!#TAA68+&e5ryd27G7`+%f1#+R0!EWUwov9ztb{Yl zSEe7WDq1NNyiX!uY4CqF8F5ow(~FyX?!LSD}abZ=$+k@U|s5C5%y2daqwaK znJQ-Bl_x55AHo_I*>6d)yefVeTcP+U8V%%RXe81=Wzmw!gB)l|v?j_d2DmBvSTw18 zhtxL3X&>M;5=_KdTW&QJxi-J>h~aQk@BEZy zMz{HrOaNeecE{~)Ya*MpE=!Tj+?b9#v-}xltGXW%P##~*#i3f|d866G zP5F+2gTI?Cp6+1Tsc(jZ@SB*+6#Yyy@QUGC4zKm^p7D1q#pOPllb>u0xgapp?zAc2 z0;%G$qw1bq-bcs)Nsj&@L-G$p>E(Ethl&ukY}CZb38_qW5h#Hu5ieN?%mS2qnuafr zY7{4TKz196aAGq{*-#~(;N&QY1X`@pwYGF8kSaP`Twb-Iqzp-a+oech=P5{qD4pq~ z^+jv_At!14jFN96tB$eKSwl5xnkWN`os{9V#gOYB#Tt&qqQJ_d>#1H?IDI%|R(XVU z?IoN1VFkId9hp`nYAsld)8n%vdwt<7d)f5^Em41o+ple?s#LMk*l~OA^>#AEjn@4V zS7B3qYXYqj0ac;u6NAL?0(O;CYJhIJiu1ViC)r|e+1%iuJH zljv92T0k)>LVVFR}>VIVq2kmxt86=U}C8F9^w0kx$f=u$yCKC$~P=ySeZ-cMo<_- zVzN7gW`&GUSeY$R)1@%)D3~`1t#wV;HtG7}F(DlIy!i*3m%PKBZA!1UFO4|QtfIG@ z)&EAhNXM;J%4EJ?rBBnL(a$)S=-t+D?sgurQG&%{u~;k?i^XEGSS%Kc#bU8oEEbE! iVzF2(7K_F5C;109)~3y1b#`k20000 Date: Thu, 28 May 2020 13:22:16 +0200 Subject: [PATCH 11/14] fixed memory leak --- bluos/bluos.cpp | 5 +-- bluos/bluos.h | 2 +- bluos/integrationpluginbluos.cpp | 66 ++++++++++++++++++++++++-------- 3 files changed, 53 insertions(+), 20 deletions(-) diff --git a/bluos/bluos.cpp b/bluos/bluos.cpp index 69bd7e83..33838bbc 100644 --- a/bluos/bluos.cpp +++ b/bluos/bluos.cpp @@ -55,9 +55,8 @@ QHostAddress BluOS::hostAddress() return m_hostAddress; } -QUuid BluOS::getStatus() +void BluOS::getStatus() { - QUuid requestId = QUuid::createUuid(); QUrl url; url.setScheme("http"); url.setHost(m_hostAddress.toString()); @@ -81,7 +80,7 @@ QUuid BluOS::getStatus() //qCDebug(dcBluOS()) << "Get Status:" << data; parseState(data); }); - return requestId; + return; } QUuid BluOS::setVolume(uint volume) diff --git a/bluos/bluos.h b/bluos/bluos.h index 65771f67..6263bd34 100644 --- a/bluos/bluos.h +++ b/bluos/bluos.h @@ -102,7 +102,7 @@ public: QHostAddress hostAddress(); // Status Queries - QUuid getStatus(); + void getStatus(); // Volume Control QUuid setVolume(uint volume); diff --git a/bluos/integrationpluginbluos.cpp b/bluos/integrationpluginbluos.cpp index ec1000b2..4b81efbf 100644 --- a/bluos/integrationpluginbluos.cpp +++ b/bluos/integrationpluginbluos.cpp @@ -100,8 +100,9 @@ void IntegrationPluginBluOS::setupThing(ThingSetupInfo *info) m_asyncSetup.insert(bluos, info); bluos->getStatus(); // In case the setup is cancelled before we finish it... - connect(info, &QObject::destroyed, this, [this, bluos]() { + connect(info, &ThingSetupInfo::aborted, this, [this, bluos] { m_asyncSetup.remove(bluos); + bluos->deleteLater(); }); return; } else { @@ -111,7 +112,7 @@ void IntegrationPluginBluOS::setupThing(ThingSetupInfo *info) void IntegrationPluginBluOS::postSetupThing(Thing *thing) { - Q_UNUSED(thing); + Q_UNUSED(thing) if (!m_pluginTimer) { m_pluginTimer = hardwareManager()->pluginTimerManager()->registerTimer(10); @@ -222,6 +223,8 @@ void IntegrationPluginBluOS::browseThing(BrowseResult *result) if (thing->thingClassId() == bluosPlayerThingClassId) { BluOS *bluos = m_bluos.value(thing->id()); if (!bluos) { + qCWarning(dcBluOS()) << "Could not find any BluOS object that belongs to" << thing->name(); + result->finish(Thing::ThingErrorHardwareNotAvailable, "BluOS connection not properly initialized"); return; } if (result->itemId() == "presets") { @@ -270,6 +273,7 @@ void IntegrationPluginBluOS::browserItem(BrowserItemResult *result) if (thing->thingClassId() == bluosPlayerThingClassId) { BluOS *bluos = m_bluos.value(thing->id()); if (!bluos) { + qCWarning(dcBluOS()) << "Could not find any BluOS object that belongs to" << thing->name(); return; } if (result->itemId() == "presets") { @@ -291,8 +295,10 @@ void IntegrationPluginBluOS::executeBrowserItem(BrowserActionInfo *info) Thing *thing = info->thing(); if (thing->thingClassId() == bluosPlayerThingClassId) { BluOS *bluos = m_bluos.value(thing->id()); - if (!bluos) + if (!bluos) { + qCWarning(dcBluOS()) << "Could not find any BluOS object that belongs to" << thing->name(); return; + } if (info->browserAction().itemId().startsWith("presets")) { QUuid requestId; @@ -302,8 +308,10 @@ void IntegrationPluginBluOS::executeBrowserItem(BrowserActionInfo *info) connect(info, &BrowserActionInfo::aborted, this, [this, requestId]{m_asyncExecuteBrowseItems.remove(requestId);}); } else if (info->browserAction().itemId().startsWith("grouping")) { //TODO Grouping + //Test devices are required } else { //TODO Sources + //Test services are required } } } @@ -319,12 +327,15 @@ void IntegrationPluginBluOS::onConnectionChanged(bool connected) info->thing()->setStateValue(bluosPlayerConnectedStateTypeId, true); info->finish(Thing::ThingErrorNoError); } else { + bluos->deleteLater(); info->finish(Thing::ThingErrorSetupFailed); } } else { Thing *thing = myThings().findById(m_bluos.key(bluos)); - if (!thing) + if (!thing) { + qCWarning(dcBluOS()) << "Could not find any Thing that belongs to the BluOS object"; return; + } thing->setStateValue(bluosPlayerConnectedStateTypeId, connected); } } @@ -333,8 +344,10 @@ void IntegrationPluginBluOS::onStatusResponseReceived(const BluOS::StatusRespons { BluOS *bluos = static_cast(sender()); Thing *thing = myThings().findById(m_bluos.key(bluos)); - if (!thing) + if (!thing){ + qCWarning(dcBluOS()) << "Could not find any Thing that belongs to this BluOS object"; return; + } thing->setStateValue(bluosPlayerArtistStateTypeId, status.Artist); thing->setStateValue(bluosPlayerCollectionStateTypeId, status.Album); thing->setStateValue(bluosPlayerTitleStateTypeId, status.Title); @@ -398,8 +411,10 @@ void IntegrationPluginBluOS::onVolumeReceived(int volume, bool mute) { BluOS *bluos = static_cast(sender()); Thing *thing = myThings().findById(m_bluos.key(bluos)); - if (!thing) + if (!thing){ + qCWarning(dcBluOS()) << "Could not find any Thing that belongs to this BluOS object"; return; + } thing->setStateValue(bluosPlayerMuteStateTypeId, mute); thing->setStateValue(bluosPlayerVolumeStateTypeId, volume); } @@ -417,8 +432,10 @@ void IntegrationPluginBluOS::onRepeatModeReceived(BluOS::RepeatMode mode) { BluOS *bluos = static_cast(sender()); Thing *thing = myThings().findById(m_bluos.key(bluos)); - if (!thing) + if (!thing){ + qCWarning(dcBluOS()) << "Could not find any Thing that belongs to this BluOS object"; return; + } switch (mode) { case BluOS::RepeatMode::All: thing->setStateValue(bluosPlayerRepeatStateTypeId, "All"); @@ -437,10 +454,14 @@ void IntegrationPluginBluOS::onPresetsReceived(QUuid requestId, const QList(sender()); Thing *thing = myThings().findById(m_bluos.key(bluos)); - if (!thing) - return; + if (m_asyncBrowseResults.contains(requestId)) { BrowseResult *result = m_asyncBrowseResults.take(requestId); + if (!thing) { + qCWarning(dcBluOS()) << "Could not find any Thing that belongs to this browse result"; + result->finish(Thing::ThingErrorHardwareNotAvailable); + return; + } foreach(BluOS::Preset preset, presets) { qCDebug(dcBluOS()) << "Preset added" << preset.Name << preset.Id << preset.Url; BrowserItem item("presets&"+QString::number(preset.Id), preset.Name, false, true); @@ -451,7 +472,8 @@ void IntegrationPluginBluOS::onPresetsReceived(QUuid requestId, const QListfinish(Thing::ThingErrorItemNotFound); + //For future browsing features } } @@ -459,10 +481,14 @@ void IntegrationPluginBluOS::onSourcesReceived(QUuid requestId, const QList(sender()); Thing *thing = myThings().findById(m_bluos.key(bluos)); - if (!thing) - return; + if (m_asyncBrowseResults.contains(requestId)) { BrowseResult *result = m_asyncBrowseResults.take(requestId); + if (!thing) { + qCWarning(dcBluOS()) << "Could not find any Thing that belongs to this browse result"; + result->finish(Thing::ThingErrorHardwareNotAvailable); + return; + } foreach(BluOS::Source source, sources) { qCDebug(dcBluOS()) << "Source added" << source.Text << source.BrowseKey << source.Type; MediaBrowserItem item; @@ -493,15 +519,18 @@ void IntegrationPluginBluOS::onSourcesReceived(QUuid requestId, const QListaddItem(item); } else if (source.Text == "Radio Paradise") { - item.setMediaIcon(MediaBrowserItem::MediaBrowserIconRadioParadise); + //item.setMediaIcon(MediaBrowserItem::MediaBrowserIconRadioParadise); //result->addItem(item); + //Needs testing before continuing } } result->finish(Thing::ThingErrorNoError); } + if (m_asyncBrowseItemResults.contains(requestId)) { BrowserItemResult *result = m_asyncBrowseItemResults.take(requestId); - Q_UNUSED(result) + result->finish(Thing::ThingErrorItemNotFound); + //For future browsing features } } @@ -509,10 +538,15 @@ void IntegrationPluginBluOS::onBrowseResultReceived(QUuid requestId, const QList { BluOS *bluos = static_cast(sender()); Thing *thing = myThings().findById(m_bluos.key(bluos)); - if (!thing) - return; + if (m_asyncBrowseResults.contains(requestId)) { BrowseResult *result = m_asyncBrowseResults.take(requestId); + + if (!thing) { + qCWarning(dcBluOS()) << "Could not find any Thing that belongs to this browse result"; + result->finish(Thing::ThingErrorHardwareNotAvailable); + return; + } foreach(BluOS::Source source, sources) { qCDebug(dcBluOS()) << "Source added" << source.Text << source.BrowseKey << source.Type; MediaBrowserItem item; From 05eaf3bd29c40b46ae08a298b41b4788fc8dfeb7 Mon Sep 17 00:00:00 2001 From: "bernhard.trinnes" Date: Thu, 28 May 2020 13:23:40 +0200 Subject: [PATCH 12/14] make german translation work for all countries --- ...8c61-de_DE.ts => 71dd25b3-37ef-4b27-abca-24989fa38c61-de.ts} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename bluos/translations/{71dd25b3-37ef-4b27-abca-24989fa38c61-de_DE.ts => 71dd25b3-37ef-4b27-abca-24989fa38c61-de.ts} (99%) diff --git a/bluos/translations/71dd25b3-37ef-4b27-abca-24989fa38c61-de_DE.ts b/bluos/translations/71dd25b3-37ef-4b27-abca-24989fa38c61-de.ts similarity index 99% rename from bluos/translations/71dd25b3-37ef-4b27-abca-24989fa38c61-de_DE.ts rename to bluos/translations/71dd25b3-37ef-4b27-abca-24989fa38c61-de.ts index 91a9f642..7ee43f88 100644 --- a/bluos/translations/71dd25b3-37ef-4b27-abca-24989fa38c61-de_DE.ts +++ b/bluos/translations/71dd25b3-37ef-4b27-abca-24989fa38c61-de.ts @@ -1,6 +1,6 @@ - + BluOS From 365f7f477bd757a78ff74cafb56e543ca67a7a43 Mon Sep 17 00:00:00 2001 From: "bernhard.trinnes" Date: Thu, 28 May 2020 13:24:51 +0200 Subject: [PATCH 13/14] simplified README --- bluos/README.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/bluos/README.md b/bluos/README.md index 68f37ec0..92a0f321 100644 --- a/bluos/README.md +++ b/bluos/README.md @@ -5,8 +5,6 @@ This integration allows to control audio devices based on BluOS. BluOS is an ope ## Supported Things * BluOS Player - * Auto discovery setup - * Auto rediscovery on IP address change * Multimedia control * Volume * Browsing Presets From 0fc335af515e0f544f5d66ec39514ebae2ec36df Mon Sep 17 00:00:00 2001 From: "bernhard.trinnes" Date: Wed, 3 Jun 2020 19:13:14 +0200 Subject: [PATCH 14/14] added mediaplayer interface --- bluos/bluos.cpp | 37 +++++++++++++++++++------------ bluos/bluos.pro | 2 -- bluos/integrationpluginbluos.json | 14 +++++++++++- 3 files changed, 36 insertions(+), 17 deletions(-) diff --git a/bluos/bluos.cpp b/bluos/bluos.cpp index 33838bbc..eea5df2d 100644 --- a/bluos/bluos.cpp +++ b/bluos/bluos.cpp @@ -63,10 +63,9 @@ void BluOS::getStatus() url.setPort(m_port); url.setPath("/Status"); QNetworkReply *reply = m_networkManager->get(QNetworkRequest(url)); + connect(reply, &QNetworkReply::finished, reply, &QNetworkReply::deleteLater); connect(reply, &QNetworkReply::finished, this, [reply, this] { - 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) { @@ -98,8 +97,9 @@ QUuid BluOS::setVolume(uint volume) url.setPath("/Volume"); url.setQuery(query); QNetworkReply *reply = m_networkManager->get(QNetworkRequest(url)); + connect(reply, &QNetworkReply::finished, reply, &QNetworkReply::deleteLater); connect(reply, &QNetworkReply::finished, this, [requestId, reply, this] { - reply->deleteLater(); + int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); // Check HTTP status code @@ -150,8 +150,9 @@ QUuid BluOS::setMute(bool mute) url.setQuery(query); QNetworkReply *reply = m_networkManager->get(QNetworkRequest(url)); + connect(reply, &QNetworkReply::finished, reply, &QNetworkReply::deleteLater); connect(reply, &QNetworkReply::finished, this, [requestId, reply, this] { - reply->deleteLater(); + int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); // Check HTTP status code @@ -204,8 +205,9 @@ QUuid BluOS::setShuffle(bool shuffle) query.addQueryItem("state", QString::number(shuffle)); url.setQuery(query); QNetworkReply *reply = m_networkManager->get(QNetworkRequest(url)); + connect(reply, &QNetworkReply::finished, reply, &QNetworkReply::deleteLater); connect(reply, &QNetworkReply::finished, this, [requestId, reply, this] { - reply->deleteLater(); + int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); // Check HTTP status code @@ -250,8 +252,9 @@ QUuid BluOS::setRepeat(RepeatMode repeatMode) query.addQueryItem("state", QString::number(repeatMode)); url.setQuery(query); QNetworkReply *reply = m_networkManager->get(QNetworkRequest(url)); + connect(reply, &QNetworkReply::finished, reply, &QNetworkReply::deleteLater); connect(reply, &QNetworkReply::finished, this, [requestId, reply, this] { - reply->deleteLater(); + int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); // Check HTTP status code @@ -295,8 +298,9 @@ QUuid BluOS::listPresets() url.setPort(m_port); url.setPath("/Presets"); QNetworkReply *reply = m_networkManager->get(QNetworkRequest(url)); + connect(reply, &QNetworkReply::finished, reply, &QNetworkReply::deleteLater); connect(reply, &QNetworkReply::finished, this, [requestId, reply, this] { - reply->deleteLater(); + int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); // Check HTTP status code @@ -342,7 +346,6 @@ QUuid BluOS::listPresets() } emit presetsReceived(requestId, presetList); }); - return requestId; } @@ -360,8 +363,9 @@ QUuid BluOS::loadPreset(int preset) url.setQuery(query); qCDebug(dcBluOS()) << "Loading preset" << url.toString(); QNetworkReply *reply = m_networkManager->get(QNetworkRequest(url)); + connect(reply, &QNetworkReply::finished, reply, &QNetworkReply::deleteLater); connect(reply, &QNetworkReply::finished, this, [requestId, reply, this] { - reply->deleteLater(); + int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); // Check HTTP status code @@ -389,8 +393,9 @@ QUuid BluOS::getSources() url.setPort(m_port); url.setPath("/Browse"); QNetworkReply *reply = m_networkManager->get(QNetworkRequest(url)); + connect(reply, &QNetworkReply::finished, reply, &QNetworkReply::deleteLater); connect(reply, &QNetworkReply::finished, this, [requestId, reply, this] { - reply->deleteLater(); + int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); // Check HTTP status code @@ -454,8 +459,9 @@ QUuid BluOS::browseSource(const QString &key) query.addQueryItem("key", key); url.setQuery(query); QNetworkReply *reply = m_networkManager->get(QNetworkRequest(url)); + connect(reply, &QNetworkReply::finished, reply, &QNetworkReply::deleteLater); connect(reply, &QNetworkReply::finished, this, [requestId, reply, this] { - reply->deleteLater(); + int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); // Check HTTP status code @@ -522,8 +528,9 @@ QUuid BluOS::addGroupPlayer(QHostAddress address, int port) query.addQueryItem("port", QString::number(port)); url.setQuery(query); QNetworkReply *reply = m_networkManager->get(QNetworkRequest(url)); + connect(reply, &QNetworkReply::finished, reply, &QNetworkReply::deleteLater); connect(reply, &QNetworkReply::finished, this, [reply, this] { - reply->deleteLater(); + int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); // Check HTTP status code @@ -556,8 +563,9 @@ QUuid BluOS::removeGroupPlayer(QHostAddress address, int port) query.addQueryItem("port", QString::number(port)); url.setQuery(query); QNetworkReply *reply = m_networkManager->get(QNetworkRequest(url)); + connect(reply, &QNetworkReply::finished, reply, &QNetworkReply::deleteLater); connect(reply, &QNetworkReply::finished, this, [reply, this] { - reply->deleteLater(); + int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); // Check HTTP status code @@ -607,8 +615,9 @@ QUuid BluOS::playBackControl(BluOS::PlaybackCommand command) request.setUrl(url); QNetworkReply *reply = m_networkManager->get(request); qCDebug(dcBluOS()) << "Sending request" << request.url(); + connect(reply, &QNetworkReply::finished, reply, &QNetworkReply::deleteLater); connect(reply, &QNetworkReply::finished, this, [requestId, reply, this] { - reply->deleteLater(); + int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); // Check HTTP status code diff --git a/bluos/bluos.pro b/bluos/bluos.pro index a878a4c7..4297114e 100644 --- a/bluos/bluos.pro +++ b/bluos/bluos.pro @@ -2,8 +2,6 @@ include(../plugins.pri) QT += network -TARGET = $$qtLibraryTarget(nymea_integrationpluginbluos) - SOURCES += \ integrationpluginbluos.cpp \ bluos.cpp \ diff --git a/bluos/integrationpluginbluos.json b/bluos/integrationpluginbluos.json index 3d946475..f9debc8f 100644 --- a/bluos/integrationpluginbluos.json +++ b/bluos/integrationpluginbluos.json @@ -13,7 +13,7 @@ "name": "bluosPlayer", "displayName": "BluOS player", "createMethods": ["discovery"], - "interfaces": ["extendedvolumecontroller", "mediametadataprovider", "shufflerepeat", "connectable"], + "interfaces": ["mediaplayer", "extendedvolumecontroller", "mediametadataprovider", "shufflerepeat", "connectable"], "browsable": true, "paramTypes":[ { @@ -150,6 +150,18 @@ "displayNameEvent": "Group changed", "type": "QString", "defaultValue": "" + }, + { + "id": "e5222e93-fa14-49de-950f-2f605ce8927b", + "name": "playerType", + "displayName": "Player type", + "displayNameEvent": "Player type changed", + "possibleValues": [ + "audio", + "video" + ], + "type": "QString", + "defaultValue": "audio" } ], "actionTypes": [