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