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