diff --git a/denon/avrconnection.cpp b/denon/avrconnection.cpp new file mode 100644 index 00000000..91440d48 --- /dev/null +++ b/denon/avrconnection.cpp @@ -0,0 +1,261 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * * + * Copyright (C) 2015 Simon Stürz * + * Copyright (C) 2019 Bernhard Trinnes * + * * + * This file is part of nymea. * + * * + * This library is free software; you can redistribute it and/or * + * modify it under the terms of the GNU Lesser General Public * + * License as published by the Free Software Foundation; either * + * version 2.1 of the License, or (at your option) any later version. * + * * + * This library 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 library; If not, see * + * . * + * * + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +#include "avrconnection.h" +#include "extern-plugininfo.h" + +AvrConnection::AvrConnection(const QHostAddress &hostAddress, const int &port, QObject *parent) : + QObject(parent), + m_hostAddress(hostAddress), + m_port(port) +{ + m_socket = new QTcpSocket(this); + + connect(m_socket, &QTcpSocket::connected, this, &AvrConnection::onConnected); + connect(m_socket, &QTcpSocket::disconnected, this, &AvrConnection::onDisconnected); + connect(m_socket, &QTcpSocket::readyRead, this, &AvrConnection::readData); + // Note: error signal will be interpreted as function, not as signal in C++11 + connect(m_socket, SIGNAL(error(QAbstractSocket::SocketError)), this, SLOT(onError(QAbstractSocket::SocketError))); +} + +AvrConnection::~AvrConnection() +{ + m_socket->close(); +} + +void AvrConnection::connectDevice() +{ + if (m_socket->state() == QAbstractSocket::ConnectingState) { + return; + } + m_socket->connectToHost(m_hostAddress, m_port); +} + +void AvrConnection::disconnectDevice() +{ + m_socket->close(); +} + +QHostAddress AvrConnection::hostAddress() const +{ + return m_hostAddress; +} + +int AvrConnection::port() const +{ + return m_port; +} + +bool AvrConnection::connected() +{ + return m_socket->isOpen(); +} + +void AvrConnection::getAllStatus() +{ + sendCommand("PW?\rSI?\rMV?\rMS?\rMU?\r"); +} + +void AvrConnection::getChannel() +{ + sendCommand("SI?\r"); +} + +void AvrConnection::getVolume() +{ + sendCommand("MV?\r"); +} + +void AvrConnection::getMute() +{ + sendCommand("MU?\r"); +} + +void AvrConnection::getPower() +{ + sendCommand("PW?\r"); +} + +void AvrConnection::getSurroundMode() +{ + sendCommand("MS?\r"); +} + +void AvrConnection::sendCommand(const QByteArray &message) +{ + m_socket->write(message); +} + +void AvrConnection::setChannel(const QByteArray &channel) +{ + QByteArray cmd = "SI" + channel + "\r"; + qCDebug(dcDenon) << "Change to channel:" << cmd; + sendCommand(cmd); +} + +void AvrConnection::setVolume(int volume) +{ + qCDebug(dcDenon) << "Set volume" << volume; + QByteArray cmd = "MV" + QByteArray::number(volume) + "\r"; + sendCommand(cmd); +} + +void AvrConnection::setMute(bool mute) +{ + qCDebug(dcDenon) << "Set mute" << mute; + QByteArray cmd; + if (mute) { + cmd = "MUON\r"; + } else { + cmd = "MUOFF\r"; + } + sendCommand(cmd); +} + +void AvrConnection::setPower(bool power) +{ + qCDebug(dcDenon) << "Set power" << power; + QByteArray cmd; + if (power) { + cmd = "PWON\r"; + } else { + cmd = "PWSTANDBY\r"; + } + sendCommand(cmd); +} + +void AvrConnection::setSurroundMode(const QByteArray &surroundMode) +{ + qCDebug(dcDenon) << "Set surround mode" << surroundMode; + QByteArray cmd = "MS" + surroundMode + "\r"; + sendCommand(cmd); +} + +void AvrConnection::increaseVolume() +{ + qCDebug(dcDenon) << "Execute volume increase"; + QByteArray cmd = "MVUP\r"; + sendCommand(cmd); +} + +void AvrConnection::decreaseVolume() +{ + qCDebug(dcDenon) << "Execute volume decrease"; + QByteArray cmd = "MVDOWN\r"; + sendCommand(cmd); +} + +void AvrConnection::onConnected() +{ + qCDebug(dcDenon) << "connected successfully to" << hostAddress().toString() << port(); + emit connectionStatusChanged(true); +} + +void AvrConnection::onDisconnected() +{ + qCDebug(dcDenon) << "disconnected from" << hostAddress().toString() << port(); + emit connectionStatusChanged(false); +} + +void AvrConnection::onError(QAbstractSocket::SocketError socketError) +{ + qCWarning(dcDenon) << "socket error:" << socketError << m_socket->errorString(); + emit socketErrorOccured(socketError); +} + +void AvrConnection::readData() +{ + QByteArray data = m_socket->readAll(); + qCDebug(dcDenon) << "Data received" << data; + + if (data.contains("MV") && !data.contains("MAX")){ + int index = data.indexOf("MV"); + int volume = data.mid(index+2, 2).toInt(); + emit volumeChanged(volume); + } + + if (data.left(2).contains("SI")) { + QByteArray cmd; + if (data.contains("TUNER")) { + cmd = "TUNER"; + } else if (data.contains("DVD")) { + cmd = "DVD"; + } else if (data.contains("BD")) { + cmd = "BD"; + } else if (data.contains("TV")) { + cmd = "TV"; + } else if (data.contains("SAT/CBL")) { + cmd = "SAT/CBL"; + } else if (data.contains("MPLAY")) { + cmd = "MPLAY"; + } else if (data.contains("GAME")) { + cmd = "GAME"; + } else if (data.contains("AUX1")) { + cmd = "AUX1"; + } else if (data.contains("NET")) { + cmd = "NET"; + } else if (data.contains("PANDORA")) { + cmd = "PANDORA"; + } else if (data.contains("SIRIUSXM")) { + cmd = "SIRIUSXM"; + } else if (data.contains("SPOTIFY")) { + cmd = "SPOTIFY"; + } else if (data.contains("FLICKR")) { + cmd = "FLICKR"; + } else if (data.contains("FAVORITES")) { + cmd = "FAVORITES"; + } else if (data.contains("IRADIO")) { + cmd = "IRADIO"; + } else if (data.contains("SERVER")) { + cmd = "SERVER"; + } else if (data.contains("USB/IPOD")) { + cmd = "USB/IPOD"; + } else if (data.contains("IPD")) { + cmd = "IPD"; + } else if (data.contains("IRP")) { + cmd = "IRP"; + } else if (data.contains("FVP")) { + cmd = "FVP"; + } + emit channelChanged(cmd); + } + + if (data.contains("PWON")) { + emit powerChanged(true); + } + if (data.contains("PWSTANDBY")) { + emit powerChanged(false); + } + if (data.contains("MUON")) { + emit muteChanged(false); + } + if (data.contains("MUOFF")) { + emit muteChanged(false); + } + + if (data.left(2).contains("MS")) { + data.remove(0, 2); + QByteArray cmd = data; + emit surroundModeChanged(cmd); + } +} diff --git a/denon/denonconnection.h b/denon/avrconnection.h similarity index 63% rename from denon/denonconnection.h rename to denon/avrconnection.h index 5241aafb..c64297f8 100644 --- a/denon/denonconnection.h +++ b/denon/avrconnection.h @@ -1,7 +1,7 @@ /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * - * Copyright (C) 2015 Simon Stürz * - * Copyright (C) 2016 Bernhard Trinnes * + * Copyright (C) 2015 Simon Stürz * + * Copyright (C) 2019 Bernhard Trinnes * * * * This file is part of nymea. * * * @@ -21,36 +21,48 @@ * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ -#ifndef DENONCONNECTION_H -#define DENONCONNECTION_H +#ifndef AVRCONNECTION_H +#define AVRCONNECTION_H #include #include #include -class DenonConnection : public QObject +class AvrConnection : public QObject { Q_OBJECT public: - explicit DenonConnection(const QHostAddress &hostAddress, const int &port = 23, QObject *parent = 0); - ~DenonConnection(); + explicit AvrConnection(const QHostAddress &hostAddress, const int &port = 23, QObject *parent = nullptr); + ~AvrConnection(); - void connectDenon(); - void disconnectDenon(); + void connectDevice(); + void disconnectDevice(); QHostAddress hostAddress() const; int port() const; - bool connected(); - void sendData(const QByteArray &message); + void getAllStatus(); + void getChannel(); + void getVolume(); + void getMute(); + void getPower(); + void getSurroundMode(); + void setChannel(const QByteArray &channel); + void setVolume(int volume); + void setMute(bool mute); + void setPower(bool power); + void setSurroundMode(const QByteArray &surroundMode); + + void increaseVolume(); + void decreaseVolume(); private: - QTcpSocket *m_socket; - + QTcpSocket *m_socket = nullptr; QHostAddress m_hostAddress; int m_port; - bool m_connected; + + void sendCommand(const QByteArray &message); private slots: void onConnected(); @@ -58,13 +70,14 @@ private slots: void onError(QAbstractSocket::SocketError socketError); void readData(); - void setConnected(const bool &connected); - signals: void socketErrorOccured(QAbstractSocket::SocketError socketError); - void connectionStatusChanged(); - void dataReady(const QByteArray &data); - + void connectionStatusChanged(bool status); + void volumeChanged(int volume); + void muteChanged(bool mute); + void channelChanged(const QByteArray &channel); + void powerChanged(bool power); + void surroundModeChanged(const QByteArray &surroundMode); }; -#endif // DENONCONNECTION_H +#endif // AVRCONNECTION_H diff --git a/denon/denon.pro b/denon/denon.pro index 6a613690..bc6a7b8d 100644 --- a/denon/denon.pro +++ b/denon/denon.pro @@ -6,8 +6,13 @@ TARGET = $$qtLibraryTarget(nymea_deviceplugindenon) SOURCES += \ deviceplugindenon.cpp \ - denonconnection.cpp + heos.cpp \ + heosplayer.cpp \ + avrconnection.cpp HEADERS += \ deviceplugindenon.h \ - denonconnection.h + heos.h \ + heosplayer.h \ + avrconnection.h \ + heostypes.h diff --git a/denon/denonconnection.cpp b/denon/denonconnection.cpp deleted file mode 100644 index d55e15ed..00000000 --- a/denon/denonconnection.cpp +++ /dev/null @@ -1,108 +0,0 @@ -/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * - * * - * Copyright (C) 2015 Simon Stürz * - * Copyright (C) 2016 Bernhard Trinnes * - * * - * This file is part of nymea. * - * * - * This library is free software; you can redistribute it and/or * - * modify it under the terms of the GNU Lesser General Public * - * License as published by the Free Software Foundation; either * - * version 2.1 of the License, or (at your option) any later version. * - * * - * This library 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 library; If not, see * - * . * - * * - * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ - -#include "denonconnection.h" -#include "extern-plugininfo.h" - -DenonConnection::DenonConnection(const QHostAddress &hostAddress, const int &port, QObject *parent) : - QObject(parent), - m_hostAddress(hostAddress), - m_port(port), - m_connected(false) -{ - m_socket = new QTcpSocket(this); - - connect(m_socket, &QTcpSocket::connected, this, &DenonConnection::onConnected); - connect(m_socket, &QTcpSocket::disconnected, this, &DenonConnection::onDisconnected); - connect(m_socket, &QTcpSocket::readyRead, this, &DenonConnection::readData); - // Note: error signal will be interpreted as function, not as signal in C++11 - connect(m_socket, SIGNAL(error(QAbstractSocket::SocketError)), this, SLOT(onError(QAbstractSocket::SocketError))); -} - -DenonConnection::~DenonConnection() -{ - m_socket->close(); -} - -void DenonConnection::connectDenon() -{ - if (m_socket->state() == QAbstractSocket::ConnectingState) { - return; - } - m_socket->connectToHost(m_hostAddress, m_port); -} - -void DenonConnection::disconnectDenon() -{ - m_socket->close(); -} - -QHostAddress DenonConnection::hostAddress() const -{ - return m_hostAddress; -} - -int DenonConnection::port() const -{ - return m_port; -} - -bool DenonConnection::connected() -{ - return m_connected; -} - -void DenonConnection::sendData(const QByteArray &message) -{ - m_socket->write(message); -} - -void DenonConnection::onConnected() -{ - qCDebug(dcDenon) << "connected successfully to" << hostAddress().toString() << port(); - setConnected(true); -} - -void DenonConnection::onDisconnected() -{ - qCDebug(dcDenon) << "disconnected from" << hostAddress().toString() << port(); - setConnected(false); -} - -void DenonConnection::onError(QAbstractSocket::SocketError socketError) -{ - qCWarning(dcDenon) << "socket error:" << socketError << m_socket->errorString(); - emit socketErrorOccured(socketError); -} - -void DenonConnection::readData() -{ - QByteArray data = m_socket->readAll(); - emit dataReady(QString(data).toUtf8()); -} - -void DenonConnection::setConnected(const bool &connected) -{ - m_connected = connected; - emit connectionStatusChanged(); -} diff --git a/denon/deviceplugindenon.cpp b/denon/deviceplugindenon.cpp index f3a7ec2d..be3b9c7c 100644 --- a/denon/deviceplugindenon.cpp +++ b/denon/deviceplugindenon.cpp @@ -1,7 +1,7 @@ /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * - * Copyright (C) 2015 Simon Stürz * - * Copyright (C) 2016 Bernhard Trinnes * + * Copyright (C) 2015 Simon Stürz * + * Copyright (C) 2019 Bernhard Trinnes * * * * This file is part of nymea. * * * @@ -21,112 +21,301 @@ * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ +/*! + \page denon.html + \title Denon + \brief Plugin for Denon AV and Heos Devices + + \ingroup plugins + \ingroup nymea-plugins + + This plug-in supports the + \l {http://www.denon.de/de/product/hometheater/avreceivers/avrx1000}{Denon AV Amplifier AVR-X1000} + + \chapter Plugin properties + Following JSON file contains the definition and the description of all available \l{DeviceClass}{DeviceClasses} + and \l{Vendor}{Vendors} of this \l{DevicePlugin}. + + For more details how to read this JSON file please check out the documentation for \l{The plugin JSON File}. + + \quotefile plugins/deviceplugins/denon/deviceplugindenon.json +*/ + #include "deviceplugindenon.h" #include "plugininfo.h" +#include "devices/device.h" +#include "network/networkaccessmanager.h" +#include "network/upnp/upnpdiscovery.h" +#include "network/upnp/upnpdiscoveryreply.h" +#include "platform/platformzeroconfcontroller.h" +#include "network/zeroconf/zeroconfservicebrowser.h" + +#include +#include +#include +#include DevicePluginDenon::DevicePluginDenon() { - } -DevicePluginDenon::~DevicePluginDenon() +Device::DeviceError DevicePluginDenon::discoverDevices(const DeviceClassId &deviceClassId, const ParamList ¶ms) { - hardwareManager()->pluginTimerManager()->unregisterTimer(m_pluginTimer); -} + Q_UNUSED(params) -void DevicePluginDenon::init() -{ - m_pluginTimer = hardwareManager()->pluginTimerManager()->registerTimer(15); - connect(m_pluginTimer, &PluginTimer::timeout, this, &DevicePluginDenon::onPluginTimer); + if (deviceClassId == AVRX1000DeviceClassId) { + if (!m_serviceBrowser) { + m_serviceBrowser = hardwareManager()->zeroConfController()->createServiceBrowser(); + connect(m_serviceBrowser, &ZeroConfServiceBrowser::serviceEntryAdded, this, &DevicePluginDenon::onAvahiServiceEntryAdded); + connect(m_serviceBrowser, &ZeroConfServiceBrowser::serviceEntryRemoved, this, &DevicePluginDenon::onAvahiServiceEntryRemoved); + } + QStringList discoveredIds; + + QList deviceDescriptors; + foreach (const ZeroConfServiceEntry &service, m_serviceBrowser->serviceEntries()) { + if (service.txt().contains("am=AVRX1000")) { + + QString id = service.name().split("@").first(); + QString name = service.name().split("@").last(); + QString address = service.hostAddress().toString(); + qCDebug(dcDenon) << "service discovered" << name << "ID:" << id; + if (discoveredIds.contains(id)) + break; + discoveredIds.append(id); + DeviceDescriptor deviceDescriptor(AVRX1000DeviceClassId, name, address); + ParamList params; + params.append(Param(AVRX1000DeviceIpParamTypeId, address)); + params.append(Param(AVRX1000DeviceIdParamTypeId, id)); + deviceDescriptor.setParams(params); + foreach (Device *existingDevice, myDevices()) { + if (existingDevice->paramValue(AVRX1000DeviceIdParamTypeId).toString() == id) { + deviceDescriptor.setDeviceId(existingDevice->id()); + break; + } + } + deviceDescriptors.append(deviceDescriptor); + } + } + + emit devicesDiscovered(AVRX1000DeviceClassId, deviceDescriptors); + return Device::DeviceErrorAsync; + } + + if (deviceClassId == heosDeviceClassId) { + /* + * 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:device:ACT-Denon:1'. + */ + UpnpDiscoveryReply *reply = hardwareManager()->upnpDiscovery()->discoverDevices(); + connect(reply, &UpnpDiscoveryReply::finished, this, &DevicePluginDenon::onUpnpDiscoveryFinished); + return Device::DeviceErrorAsync; + } + return Device::DeviceErrorDeviceClassNotFound; } Device::DeviceSetupStatus DevicePluginDenon::setupDevice(Device *device) { - qCDebug(dcDenon) << "Setup Denon device" << device->paramValue(AVRX1000DeviceIpParamTypeId).toString(); - - // Check if we already have a denon device - if (!myDevices().isEmpty()) { - qCWarning(dcDenon) << "Could not add denon device. Only one denon device allowed."; - return Device::DeviceSetupStatusFailure; + if(!m_pluginTimer) { + m_pluginTimer = hardwareManager()->pluginTimerManager()->registerTimer(60); + connect(m_pluginTimer, &PluginTimer::timeout, this, &DevicePluginDenon::onPluginTimer); } - QHostAddress address(device->paramValue(AVRX1000DeviceIpParamTypeId).toString()); - if (address.isNull()) { - qCWarning(dcDenon) << "Could not parse ip address" << device->paramValue(AVRX1000DeviceIpParamTypeId).toString(); - return Device::DeviceSetupStatusFailure; + if (device->deviceClassId() == AVRX1000DeviceClassId) { + qCDebug(dcDenon) << "Setup Denon device" << device->paramValue(AVRX1000DeviceIpParamTypeId).toString(); + + QHostAddress address(device->paramValue(AVRX1000DeviceIpParamTypeId).toString()); + if (address.isNull()) { + qCWarning(dcDenon) << "Could not parse ip address" << device->paramValue(AVRX1000DeviceIpParamTypeId).toString(); + return Device::DeviceSetupStatusFailure; + } + + AvrConnection *denonConnection = new AvrConnection(address, 23, this); + connect(denonConnection, &AvrConnection::connectionStatusChanged, this, &DevicePluginDenon::onAvrConnectionChanged); + connect(denonConnection, &AvrConnection::socketErrorOccured, this, &DevicePluginDenon::onAvrSocketError); + connect(denonConnection, &AvrConnection::channelChanged, this, &DevicePluginDenon::onAvrChannelChanged); + connect(denonConnection, &AvrConnection::powerChanged, this, &DevicePluginDenon::onAvrPowerChanged); + connect(denonConnection, &AvrConnection::volumeChanged, this, &DevicePluginDenon::onAvrVolumeChanged); + connect(denonConnection, &AvrConnection::surroundModeChanged, this, &DevicePluginDenon::onAvrSurroundModeChanged); + connect(denonConnection, &AvrConnection::muteChanged, this, &DevicePluginDenon::onAvrMuteChanged); + + m_asyncAvrSetups.append(denonConnection); + denonConnection->connectDevice(); + m_avrConnections.insert(device, denonConnection); + return Device::DeviceSetupStatusAsync; } - m_device = device; - m_denonConnection = new DenonConnection(address, 23, this); - connect(m_denonConnection.data(), &DenonConnection::connectionStatusChanged, this, &DevicePluginDenon::onConnectionChanged); - connect(m_denonConnection.data(), &DenonConnection::socketErrorOccured, this, &DevicePluginDenon::onSocketError); - connect(m_denonConnection.data(), &DenonConnection::dataReady, this, &DevicePluginDenon::onDataReceived); + if (device->deviceClassId() == heosDeviceClassId) { + qCDebug(dcDenon) << "Setup Denon device" << device->paramValue(heosDeviceIpParamTypeId).toString(); - m_asyncSetups.append(m_denonConnection); - m_denonConnection->connectDenon(); + QHostAddress address(device->paramValue(heosDeviceIpParamTypeId).toString()); + Heos *heos = new Heos(address, this); + connect(heos, &Heos::connectionStatusChanged, this, &DevicePluginDenon::onHeosConnectionChanged); + connect(heos, &Heos::playerDiscovered, this, &DevicePluginDenon::onHeosPlayerDiscovered); + connect(heos, &Heos::playStateReceived, this, &DevicePluginDenon::onHeosPlayStateReceived); + connect(heos, &Heos::repeatModeReceived, this, &DevicePluginDenon::onHeosRepeatModeReceived); + connect(heos, &Heos::shuffleModeReceived, this, &DevicePluginDenon::onHeosShuffleModeReceived); + connect(heos, &Heos::muteStatusReceived, this, &DevicePluginDenon::onHeosMuteStatusReceived); + connect(heos, &Heos::volumeStatusReceived, this, &DevicePluginDenon::onHeosVolumeStatusReceived); + connect(heos, &Heos::nowPlayingMediaStatusReceived, this, &DevicePluginDenon::onHeosNowPlayingMediaStatusReceived); - return Device::DeviceSetupStatusAsync; + m_asyncHeosSetups.append(heos); + heos->connectHeos(); + m_heos.insert(device, heos); + return Device::DeviceSetupStatusAsync; + } + + if (device->deviceClassId() == heosPlayerDeviceClassId) { + return Device::DeviceSetupStatusSuccess; + } + return Device::DeviceSetupStatusFailure; } void DevicePluginDenon::deviceRemoved(Device *device) { qCDebug(dcDenon) << "Delete " << device->name(); - if (m_denonConnection.isNull()){ - qCWarning(dcDenon) << "Invalid connection pointer" << device->id().toString(); - return; + + if (device->deviceClassId() == AVRX1000DeviceClassId) { + AvrConnection *denonConnection = m_avrConnections.value(device); + m_avrConnections.remove(device); + denonConnection->disconnectDevice(); + denonConnection->deleteLater(); + } + + if (device->deviceClassId() == heosDeviceClassId) { + if (m_avrConnections.contains(device)) { + AvrConnection *denonConnection = m_avrConnections.value(device); + m_avrConnections.remove(device); + denonConnection->disconnectDevice(); + denonConnection->deleteLater(); + } + } + + if (myDevices().empty()) { + hardwareManager()->pluginTimerManager()->unregisterTimer(m_pluginTimer); } - m_device.clear(); - m_denonConnection->disconnectDenon(); - m_denonConnection->deleteLater(); } Device::DeviceError DevicePluginDenon::executeAction(Device *device, const Action &action) { qCDebug(dcDenon) << "Execute action" << device->id() << action.id() << action.params(); if (device->deviceClassId() == AVRX1000DeviceClassId) { + AvrConnection *avrConnection = m_avrConnections.value(device); - // check connection state - if (m_denonConnection.isNull() || !m_denonConnection->connected()) - return Device::DeviceErrorHardwareNotAvailable; - - // check if the requested action is our "update" action ... if (action.actionTypeId() == AVRX1000PowerActionTypeId) { - // Print information that we are executing now the update action - qCDebug(dcDenon) << "set power action" << action.id(); - qCDebug(dcDenon) << "power: " << action.param(AVRX1000PowerActionPowerParamTypeId).value().Bool; - - if (action.param(AVRX1000PowerActionPowerParamTypeId).value().toBool() == true){ - QByteArray cmd = "PWON\r"; - qCDebug(dcDenon) << "Execute power: " << action.id() << cmd; - m_denonConnection->sendData(cmd); - } else { - QByteArray cmd = "PWSTANDBY\r"; - qCDebug(dcDenon) << "Execute power: " << action.id() << cmd; - m_denonConnection->sendData(cmd); - } - + bool power = action.param(AVRX1000PowerActionPowerParamTypeId).value().toBool(); + avrConnection->setPower(power); return Device::DeviceErrorNoError; } else if (action.actionTypeId() == AVRX1000VolumeActionTypeId) { - QByteArray vol = action.param(AVRX1000VolumeActionVolumeParamTypeId).value().toByteArray(); - QByteArray cmd = "MV" + vol + "\r"; - - qCDebug(dcDenon) << "Execute volume" << action.id() << cmd; - m_denonConnection->sendData(cmd); - + int vol = action.param(AVRX1000VolumeActionVolumeParamTypeId).value().toInt(); + avrConnection->setVolume(vol); return Device::DeviceErrorNoError; } else if (action.actionTypeId() == AVRX1000ChannelActionTypeId) { qCDebug(dcDenon) << "Execute update action" << action.id(); QByteArray channel = action.param(AVRX1000ChannelActionChannelParamTypeId).value().toByteArray(); - QByteArray cmd = "SI" + channel + "\r"; + avrConnection->setChannel(channel); + return Device::DeviceErrorNoError; - qCDebug(dcDenon) << "Change to channel:" << cmd; - m_denonConnection->sendData(cmd); + } else if (action.actionTypeId() == AVRX1000IncreaseVolumeActionTypeId) { + avrConnection->increaseVolume(); + return Device::DeviceErrorNoError; + + } else if (action.actionTypeId() == AVRX1000DecreaseVolumeActionTypeId) { + + avrConnection->decreaseVolume(); + return Device::DeviceErrorNoError; + + } else if (action.actionTypeId() == AVRX1000SurroundModeActionTypeId) { + + QByteArray surroundMode = action.param(AVRX1000SurroundModeActionSurroundModeParamTypeId).value().toByteArray(); + avrConnection->setSurroundMode(surroundMode); + return Device::DeviceErrorNoError; + } + return Device::DeviceErrorActionTypeNotFound; + } + + if (device->deviceClassId() == heosPlayerDeviceClassId) { + + Device *heosDevice = myDevices().findById(device->parentId()); + Heos *heos = m_heos.value(heosDevice); + int playerId = device->paramValue(heosPlayerDevicePlayerIdParamTypeId).toInt(); + + if (action.actionTypeId() == heosPlayerVolumeActionTypeId) { + int volume = action.param(heosPlayerVolumeActionVolumeParamTypeId).value().toInt(); + heos->setVolume(playerId, volume); + return Device::DeviceErrorNoError; + } + + if (action.actionTypeId() == heosPlayerMuteActionTypeId) { + bool mute = action.param(heosPlayerMuteActionMuteParamTypeId).value().toBool(); + heos->setMute(playerId, mute); + return Device::DeviceErrorNoError; + } + + if (action.actionTypeId() == heosPlayerPlaybackStatusActionTypeId) { + QString playbackStatus = action.param(heosPlayerPlaybackStatusActionPlaybackStatusParamTypeId).value().toString(); + if (playbackStatus == "playing") { + heos->setPlayerState(playerId, PLAYER_STATE_PLAY); + } else if (playbackStatus == "stopping") { + heos->setPlayerState(playerId, PLAYER_STATE_STOP); + } else if (playbackStatus == "pausing") { + heos->setPlayerState(playerId, PLAYER_STATE_PAUSE); + } + return Device::DeviceErrorNoError; + } + + if (action.actionTypeId() == heosPlayerShuffleActionTypeId) { + bool shuffle = action.param(heosPlayerShuffleActionShuffleParamTypeId).value().toBool(); + REPEAT_MODE repeatMode = REPEAT_MODE_OFF; + if ( device->stateValue(heosPlayerRepeatStateTypeId) == "One") { + repeatMode = REPEAT_MODE_ONE; + } else if ( device->stateValue(heosPlayerRepeatStateTypeId) == "All") { + repeatMode = REPEAT_MODE_ALL; + } + heos->setPlayMode(playerId, repeatMode, shuffle); + return Device::DeviceErrorNoError; + } + + if (action.actionTypeId() == heosPlayerSkipBackActionTypeId) { + heos->playPrevious(playerId); + return Device::DeviceErrorNoError; + } + + if (action.actionTypeId() == heosPlayerFastRewindActionTypeId) { + + return Device::DeviceErrorActionTypeNotFound; + } + + if (action.actionTypeId() == heosPlayerStopActionTypeId) { + heos->setPlayerState(playerId, PLAYER_STATE_STOP); + return Device::DeviceErrorNoError; + } + + if (action.actionTypeId() == heosPlayerPlayActionTypeId) { + heos->setPlayerState(playerId, PLAYER_STATE_PLAY); + return Device::DeviceErrorNoError; + } + + if (action.actionTypeId() == heosPlayerPauseActionTypeId) { + heos->setPlayerState(playerId, PLAYER_STATE_PAUSE); + return Device::DeviceErrorNoError; + } + + if (action.actionTypeId() == heosPlayerFastForwardActionTypeId) { + + return Device::DeviceErrorActionTypeNotFound; + } + + if (action.actionTypeId() == heosPlayerSkipNextActionTypeId) { + heos->playNext(playerId); return Device::DeviceErrorNoError; } return Device::DeviceErrorActionTypeNotFound; @@ -134,125 +323,407 @@ Device::DeviceError DevicePluginDenon::executeAction(Device *device, const Actio return Device::DeviceErrorDeviceClassNotFound; } +void DevicePluginDenon::postSetupDevice(Device *device) +{ + if (device->deviceClassId() == heosDeviceClassId) { + + Heos *heos = m_heos.value(device); + heos->getPlayers(); + } + + if (device->deviceClassId() == heosPlayerDeviceClassId) { + device->setStateValue(heosPlayerConnectedStateTypeId, true); + Device *heosDevice = myDevices().findById(device->parentId()); + Heos *heos = m_heos.value(heosDevice); + int playerId = device->paramValue(heosPlayerDevicePlayerIdParamTypeId).toInt(); + heos->getPlayerState(playerId); + heos->getPlayMode(playerId); + heos->getVolume(playerId); + heos->getMute(playerId); + heos->getNowPlayingMedia(playerId); + } +} + + void DevicePluginDenon::onPluginTimer() { - if (m_denonConnection.isNull()) - return; - - if (!m_denonConnection->connected()) { - m_denonConnection->connectDenon(); - } else { - m_denonConnection->sendData("PW?\rSI?\rMV?\r"); - } -} - -void DevicePluginDenon::onConnectionChanged() -{ - if (!m_device) - return; - - // if the device is connected - if (m_denonConnection->connected()) { - // and from the first setup - if (m_asyncSetups.contains(m_denonConnection)) { - m_asyncSetups.removeAll(m_denonConnection); - m_denonConnection->sendData("PW?\rSI?\rMV?\r"); - emit deviceSetupFinished(m_device, Device::DeviceSetupStatusSuccess); + foreach(AvrConnection *denonConnection, m_avrConnections.values()) { + if (!denonConnection->connected()) { + denonConnection->connectDevice(); + } + Device *device = m_avrConnections.key(denonConnection); + if (device->deviceClassId() == AVRX1000DeviceClassId) { + denonConnection->getAllStatus(); } } - // Set connection status - m_device->setStateValue(AVRX1000ConnectedStateTypeId, m_denonConnection->connected()); -} + foreach(Device *device, myDevices()) { -void DevicePluginDenon::onDataReceived(const QByteArray &data) -{ - qCDebug(dcDenon) << "Data received" << data; - - // if there is no device, return - if (m_device.isNull()) - return; - - if (data.contains("MV") && !data.contains("MAX")){ - int index = data.indexOf("MV"); - int vol = data.mid(index+2, 2).toInt(); - - qCDebug(dcDenon) << "Update volume:" << vol; - m_device->setStateValue(AVRX1000VolumeStateTypeId, vol); - } - - if (data.contains("SI")) { - QString cmd; - if (data.contains("TUNER")) { - cmd = "TUNER"; - } else if (data.contains("DVD")) { - cmd = "DVD"; - } else if (data.contains("BD")) { - cmd = "BD"; - } else if (data.contains("TV")) { - cmd = "TV"; - } else if (data.contains("SAT/CBL")) { - cmd = "SAT/CBL"; - } else if (data.contains("MPLAY")) { - cmd = "MPLAY"; - } else if (data.contains("GAME")) { - cmd = "GAME"; - } else if (data.contains("AUX1")) { - cmd = "AUX1"; - } else if (data.contains("NET")) { - cmd = "NET"; - } else if (data.contains("PANDORA")) { - cmd = "PANDORA"; - } else if (data.contains("SIRIUSXM")) { - cmd = "SIRIUSXM"; - } else if (data.contains("SPOTIFY")) { - cmd = "SPOTIFY"; - } else if (data.contains("FLICKR")) { - cmd = "FLICKR"; - } else if (data.contains("FAVORITES")) { - cmd = "FAVORITES"; - } else if (data.contains("IRADIO")) { - cmd = "IRADIO"; - } else if (data.contains("SERVER")) { - cmd = "SERVER"; - } else if (data.contains("USB/IPOD")) { - cmd = "USB/IPOD"; - } else if (data.contains("IPD")) { - cmd = "IPD"; - } else if (data.contains("IRP")) { - cmd = "IRP"; - } else if (data.contains("FVP")) { - cmd = "FVP"; + if (device->deviceClassId() == heosDeviceClassId) { + Heos *heos = m_heos.value(device); + heos->getPlayers(); + heos->registerForChangeEvents(true); } - qCDebug(dcDenon) << "Update channel:" << cmd; - m_device->setStateValue(AVRX1000ChannelStateTypeId, cmd); - } + if (device->deviceClassId() == heosPlayerDeviceClassId) { + Device *heosDevice = myDevices().findById(device->parentId()); + Heos *heos = m_heos.value(heosDevice); + int playerId = device->paramValue(heosPlayerDevicePlayerIdParamTypeId).toInt(); - if (data.contains("PWON")) { - qCDebug(dcDenon) << "Update power on"; - m_device->setStateValue(AVRX1000PowerStateTypeId, true); - } else if (data.contains("PWSTANDBY")) { - qCDebug(dcDenon) << "Update power off"; - m_device->setStateValue(AVRX1000PowerStateTypeId, false); + heos->getPlayerState(playerId); + heos->getPlayMode(playerId); + heos->getVolume(playerId); + heos->getMute(playerId); + heos->getNowPlayingMedia(playerId); + } } } -void DevicePluginDenon::onSocketError() +void DevicePluginDenon::onAvrConnectionChanged(bool status) { - // if there is no device, return - if (m_device.isNull()) + AvrConnection *denonConnection = static_cast(sender()); + Device *device = m_avrConnections.key(denonConnection); + if (!device) return; - // Check if setup running for this device - if (m_asyncSetups.contains(m_denonConnection)) { - qCWarning(dcDenon()) << "Could not add device. The setup failed."; - emit deviceSetupFinished(m_device, Device::DeviceSetupStatusFailure); - // Delete the connection, the device will not be added and - // the connection will be created in the next setup - m_denonConnection->deleteLater(); + if (device->deviceClassId() == AVRX1000DeviceClassId) { + // if the device is connected + if (status) { + // and from the first setup + if (m_asyncAvrSetups.contains(denonConnection)) { + m_asyncAvrSetups.removeAll(denonConnection); + + emit deviceSetupFinished(device, Device::DeviceSetupStatusSuccess); + } + } + device->setStateValue(AVRX1000ConnectedStateTypeId, denonConnection->connected()); + } +} + +void DevicePluginDenon::onAvrVolumeChanged(int volume) +{ + AvrConnection *denonConnection = static_cast(sender()); + Device *device = m_avrConnections.key(denonConnection); + if (!device) + return; + + if (device->deviceClassId() == AVRX1000DeviceClassId) { + device->setStateValue(AVRX1000VolumeStateTypeId, volume); + } +} + +void DevicePluginDenon::onAvrChannelChanged(const QByteArray &channel) +{ + AvrConnection *denonConnection = static_cast(sender()); + Device *device = m_avrConnections.key(denonConnection); + if (!device) + return; + + if (device->deviceClassId() == AVRX1000DeviceClassId) { + device->setStateValue(AVRX1000ChannelStateTypeId, channel); + } +} + +void DevicePluginDenon::onAvrMuteChanged(bool mute) +{ + AvrConnection *denonConnection = static_cast(sender()); + Device *device = m_avrConnections.key(denonConnection); + if (!device) + return; + + if (device->deviceClassId() == AVRX1000DeviceClassId) { + device->setStateValue(AVRX1000MuteStateTypeId, mute); + } +} + +void DevicePluginDenon::onAvrPowerChanged(bool power) +{ + AvrConnection *denonConnection = static_cast(sender()); + Device *device = m_avrConnections.key(denonConnection); + if (!device) + return; + + if (device->deviceClassId() == AVRX1000DeviceClassId) { + device->setStateValue(AVRX1000PowerStateTypeId, power); + } +} + +void DevicePluginDenon::onAvrSurroundModeChanged(const QByteArray &surroundMode) +{ + AvrConnection *denonConnection = static_cast(sender()); + Device *device = m_avrConnections.key(denonConnection); + if (!device) + return; + + if (device->deviceClassId() == AVRX1000DeviceClassId) { + device->setStateValue(AVRX1000SurroundModeStateTypeId, surroundMode); } } +void DevicePluginDenon::onAvrSocketError() +{ + AvrConnection *denonConnection = static_cast(sender()); + Device *device = m_avrConnections.key(denonConnection); + if (!device) + return; + if (device->deviceClassId() == AVRX1000DeviceClassId) { + + // Check if setup running for this device + if (m_asyncAvrSetups.contains(denonConnection)) { + m_asyncAvrSetups.removeAll(denonConnection); + qCWarning(dcDenon()) << "Could not add device. The setup failed."; + emit deviceSetupFinished(device, Device::DeviceSetupStatusFailure); + // Delete the connection, the device will not be added and + // the connection will be created in the next setup + denonConnection->deleteLater(); + m_avrConnections.remove(device); + } + } +} + +void DevicePluginDenon::onUpnpDiscoveryFinished() +{ + qCDebug(dcDenon()) << "Upnp discovery finished"; + UpnpDiscoveryReply *reply = static_cast(sender()); + if (reply->error() != UpnpDiscoveryReply::UpnpDiscoveryReplyErrorNoError) { + qCWarning(dcDenon()) << "Upnp discovery error" << reply->error(); + } + reply->deleteLater(); + + if (reply->deviceDescriptors().isEmpty()) { + qCDebug(dcDenon) << "No UPnP device found."; + return; + } + + QList heosDescriptors; + foreach (const UpnpDeviceDescriptor &upnpDevice, reply->deviceDescriptors()) { + + if (upnpDevice.modelName().contains("HEOS")) { + QString serialNumber = upnpDevice.serialNumber(); + if (serialNumber != "0000001") { + // child devices have serial number 0000001 + qCDebug(dcDenon) << "UPnP device found:" << upnpDevice.modelDescription() << upnpDevice.friendlyName() << upnpDevice.hostAddress().toString() << upnpDevice.modelName() << upnpDevice.manufacturer() << upnpDevice.serialNumber(); + DeviceDescriptor descriptor(heosDeviceClassId, upnpDevice.modelName(), serialNumber); + ParamList params; + foreach (Device *existingDevice, myDevices()) { + if (existingDevice->paramValue(heosDeviceSerialNumberParamTypeId).toString().contains(serialNumber, Qt::CaseSensitivity::CaseInsensitive)) { + descriptor.setDeviceId(existingDevice->id()); + break; + } + } + params.append(Param(heosDeviceModelNameParamTypeId, upnpDevice.modelName())); + params.append(Param(heosDeviceIpParamTypeId, upnpDevice.hostAddress().toString())); + params.append(Param(heosDeviceSerialNumberParamTypeId, serialNumber)); + descriptor.setParams(params); + heosDescriptors.append(descriptor); + } + } + qCDebug(dcDenon) << "UPnP device found:" << upnpDevice.modelDescription() << upnpDevice.friendlyName() << upnpDevice.hostAddress().toString() << upnpDevice.modelName() << upnpDevice.manufacturer() << upnpDevice.serialNumber(); + } + if (!heosDescriptors.isEmpty()) { + emit devicesDiscovered(heosDeviceClassId, heosDescriptors); + } +} + +void DevicePluginDenon::onHeosConnectionChanged(bool status) +{ + Heos *heos = static_cast(sender()); + heos->registerForChangeEvents(true); + Device *device = m_heos.key(heos); + if (!device) + return; + + if (device->deviceClassId() == heosDeviceClassId) { + // if the device is connected + if (status) { + // and from the first setup + if (m_asyncHeosSetups.contains(heos)) { + m_asyncHeosSetups.removeAll(heos); + heos->getPlayers(); + emit deviceSetupFinished(device, Device::DeviceSetupStatusSuccess); + } + } + device->setStateValue(heosConnectedStateTypeId, status); + // update connection status for all child devices + foreach (Device *playerDevice, myDevices()) { + if (playerDevice->deviceClassId() == heosPlayerDeviceClassId) { + if (playerDevice->parentId() == device->id()) { + playerDevice->setStateValue(heosPlayerConnectedStateTypeId, status); + } + } + } + } +} + +void DevicePluginDenon::onHeosPlayerDiscovered(HeosPlayer *heosPlayer) { + + Heos *heos = static_cast(sender()); + Device *device = m_heos.key(heos); + + foreach (Device *heosPlayerDevice, myDevices()) { + if(heosPlayerDevice->deviceClassId() == heosPlayerDeviceClassId) { + if (heosPlayerDevice->paramValue(heosPlayerDevicePlayerIdParamTypeId).toInt() == heosPlayer->playerId()) + return; + } + } + QList heosPlayerDescriptors; + DeviceDescriptor descriptor(heosPlayerDeviceClassId, heosPlayer->name(), heosPlayer->playerModel(), device->id()); + ParamList params; + params.append(Param(heosPlayerDeviceModelParamTypeId, heosPlayer->playerModel())); + params.append(Param(heosPlayerDevicePlayerIdParamTypeId, heosPlayer->playerId())); + params.append(Param(heosPlayerDeviceSerialNumberParamTypeId, heosPlayer->serialNumber())); + params.append(Param(heosPlayerDeviceVersionParamTypeId, heosPlayer->playerVersion())); + descriptor.setParams(params); + qCDebug(dcDenon) << "Found new heos player" << heosPlayer->name(); + heosPlayerDescriptors.append(descriptor); + autoDevicesAppeared(heosPlayerDeviceClassId, heosPlayerDescriptors); +} + +void DevicePluginDenon::onHeosPlayStateReceived(int playerId, PLAYER_STATE state) +{ + foreach(Device *device, myDevices().filterByParam(heosPlayerDevicePlayerIdParamTypeId, playerId)) { + if (state == PLAYER_STATE_PAUSE) { + device->setStateValue(heosPlayerPlaybackStatusStateTypeId, "Paused"); + } else if (state == PLAYER_STATE_PLAY) { + device->setStateValue(heosPlayerPlaybackStatusStateTypeId, "Playing"); + } else if (state == PLAYER_STATE_STOP) { + device->setStateValue(heosPlayerPlaybackStatusStateTypeId, "Stopped"); + } + break; + } +} + + +void DevicePluginDenon::onHeosRepeatModeReceived(int playerId, REPEAT_MODE repeatMode) +{ + foreach(Device *device, myDevices().filterByParam(heosPlayerDevicePlayerIdParamTypeId, playerId)) { + if (repeatMode == REPEAT_MODE_ALL) { + device->setStateValue(heosPlayerRepeatStateTypeId, "All"); + } else if (repeatMode == REPEAT_MODE_ONE) { + device->setStateValue(heosPlayerRepeatStateTypeId, "One"); + } else if (repeatMode == REPEAT_MODE_OFF) { + device->setStateValue(heosPlayerRepeatStateTypeId, "None"); + } + break; + } +} + +void DevicePluginDenon::onHeosShuffleModeReceived(int playerId, bool shuffle) +{ + foreach(Device *device, myDevices().filterByParam(heosPlayerDevicePlayerIdParamTypeId, playerId)) { + device->setStateValue(heosPlayerMuteStateTypeId, shuffle); + break; + } +} + +void DevicePluginDenon::onHeosMuteStatusReceived(int playerId, bool mute) +{ + foreach(Device *device, myDevices().filterByParam(heosPlayerDevicePlayerIdParamTypeId, playerId)) { + device->setStateValue(heosPlayerMuteStateTypeId, mute); + break; + } +} + +void DevicePluginDenon::onHeosVolumeStatusReceived(int playerId, int volume) +{ + foreach(Device *device, myDevices().filterByParam(heosPlayerDevicePlayerIdParamTypeId, playerId)) { + device->setStateValue(heosPlayerVolumeStateTypeId, volume); + break; + } +} + +void DevicePluginDenon::onHeosNowPlayingMediaStatusReceived(int playerId, SOURCE_ID sourceId, QString artist, QString album, QString song, QString artwork) +{ + foreach(Device *device, myDevices().filterByParam(heosPlayerDevicePlayerIdParamTypeId, playerId)) { + device->setStateValue(heosPlayerArtistStateTypeId, artist); + device->setStateValue(heosPlayerTitleStateTypeId, song); + device->setStateValue(heosPlayerArtworkStateTypeId, artwork); + device->setStateValue(heosPlayerCollectionStateTypeId, album); + QString source; + switch (sourceId) { + case SOURCE_ID_PANDORA: + source = "Pandora"; + break; + case SOURCE_ID_RHAPSODY: + source = "Rhapsody"; + break; + case SOURCE_ID_TUNEIN: + source = "TuneIn"; + break; + case SOURCE_ID_SPOTIFY: + source = "Spotify"; + break; + case SOURCE_ID_DEEZER: + source = "Deezer"; + break; + case SOURCE_ID_NAPSTER: + source = "Napster"; + break; + case SOURCE_ID_IHEARTRADIO: + source = "iHeartRadio"; + break; + case SOURCE_ID_SIRIUS_XM: + source = "Sirius XM"; + break; + case SOURCE_ID_SOUNDCLOUD: + source = "Soundcloud"; + break; + case SOURCE_ID_TIDAL: + source = "Tidal"; + break; + case SOURCE_ID_FUTURE_SERVICE_1: + source = "Unknown"; + break; + case SOURCE_ID_RDIO: + source = "Rdio"; + break; + case SOURCE_ID_AMAZON_MUSIC: + source = "Amazon Music"; + break; + case SOURCE_ID_FUTURE_SERVICE_2: + source = "Unknown"; + break; + case SOURCE_ID_MOODMIX: + source = "Moodmix"; + break; + case SOURCE_ID_JUKE: + source = "Juke"; + break; + case SOURCE_ID_FUTURE_SERVICE_3: + source = "Unkown"; + break; + case SOURCE_ID_QQMUSIC: + source = "QQMusic"; + break; + case SOURCE_ID_LOCAL_MEDIA: + source = "USB Media/DLNA Servers"; + break; + case SOURCE_ID_HEOS_PLAYLIST: + source = "HEOS Playlists"; + break; + case SOURCE_ID_HEOS_HISTORY: + source = "HEOS History"; + break; + case SOURCE_ID_HEOS_FAVORITES: + source = "HEOS Favorites"; + break; + case SOURCE_ID_HEOS_AUX: + source = "HEOS aux input"; + break; + }; + device->setStateValue(heosPlayerSourceStateTypeId, source); + break; + } +} + +void DevicePluginDenon::onAvahiServiceEntryAdded(const ZeroConfServiceEntry &serviceEntry) +{ + qCDebug(dcDenon()) << "Avahi service entry added:" << serviceEntry; +} + +void DevicePluginDenon::onAvahiServiceEntryRemoved(const ZeroConfServiceEntry &serviceEntry) +{ + qCDebug(dcDenon()) << "Avahi service entry removed:" << serviceEntry; +} diff --git a/denon/deviceplugindenon.h b/denon/deviceplugindenon.h index e144af27..84536d55 100644 --- a/denon/deviceplugindenon.h +++ b/denon/deviceplugindenon.h @@ -1,7 +1,7 @@ /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * - * Copyright (C) 2015 Simon Stürz * - * Copyright (C) 2016 Bernhard Trinnes * + * Copyright (C) 2015 Simon Stürz * + * Copyright (C) 2019 Bernhard Trinnes * * * * This file is part of nymea. * * * @@ -24,18 +24,21 @@ #ifndef DEVICEPLUGINDENON_H #define DEVICEPLUGINDENON_H +#include "heos.h" +#include "avrconnection.h" +#include "plugintimer.h" #include "devices/deviceplugin.h" +#include "network/zeroconf/zeroconfservicebrowser.h" +#include "network/zeroconf/zeroconfserviceentry.h" - +#include +#include #include #include #include #include #include -#include "plugintimer.h" -#include "denonconnection.h" - class DevicePluginDenon : public DevicePlugin { Q_OBJECT @@ -45,27 +48,50 @@ class DevicePluginDenon : public DevicePlugin public: explicit DevicePluginDenon(); - ~DevicePluginDenon(); - void init() override; + Device::DeviceError discoverDevices(const DeviceClassId &deviceClassId, const ParamList ¶ms) override; Device::DeviceSetupStatus setupDevice(Device *device) override; - void deviceRemoved(Device *device) override; + void postSetupDevice(Device * device) override; Device::DeviceError executeAction(Device *device, const Action &action) override; + void deviceRemoved(Device *device) override; private: PluginTimer *m_pluginTimer = nullptr; - QPointer m_device; - QPointer m_denonConnection; - QList m_asyncSetups; + ZeroConfServiceBrowser *m_serviceBrowser = nullptr; + + QHash m_avrConnections; + QHash m_heos; + + QList m_asyncAvrSetups; + QList m_asyncHeosSetups; + + QHash m_playerIds; + QHash m_discoveredPlayerIds; + QHash m_asyncActions; - QHash m_asyncActions; - QHash m_asyncActionReplies; private slots: void onPluginTimer(); - void onConnectionChanged(); - void onDataReceived(const QByteArray &data); - void onSocketError(); + void onUpnpDiscoveryFinished(); + + void onHeosConnectionChanged(bool status); + void onHeosPlayerDiscovered(HeosPlayer *heosPlayer); + void onHeosPlayStateReceived(int playerId, PLAYER_STATE state); + void onHeosShuffleModeReceived(int playerId, bool shuffle); + void onHeosRepeatModeReceived(int playerId, REPEAT_MODE repeatMode); + void onHeosMuteStatusReceived(int playerId, bool mute); + void onHeosVolumeStatusReceived(int playerId, int volume); + void onHeosNowPlayingMediaStatusReceived(int playerId, SOURCE_ID source, QString artist, QString album, QString Song, QString artwork); + + void onAvahiServiceEntryAdded(const ZeroConfServiceEntry &serviceEntry); + void onAvahiServiceEntryRemoved(const ZeroConfServiceEntry &serviceEntry); + void onAvrConnectionChanged(bool status); + void onAvrSocketError(); + void onAvrVolumeChanged(int volume); + void onAvrChannelChanged(const QByteArray &channel); + void onAvrMuteChanged(bool mute); + void onAvrPowerChanged(bool power); + void onAvrSurroundModeChanged(const QByteArray &surroundMode); }; #endif // DEVICEPLUGINDENON_H diff --git a/denon/deviceplugindenon.json b/denon/deviceplugindenon.json index 1f9e28ca..cce1b92b 100644 --- a/denon/deviceplugindenon.json +++ b/denon/deviceplugindenon.json @@ -12,14 +12,21 @@ "id": "1cd3d67e-aba0-450e-9e2a-483a1527aba6", "name": "AVRX1000", "displayName": "AVR X1000", - "createMethods": ["user"], + "createMethods": ["discovery"], + "interfaces": ["extendedvolumecontroller", "connectable", "power"], "paramTypes": [ { "id": "a54b98b4-b78f-41dd-a257-14425c6cf9ab", "name": "ip", - "displayName": "ip", + "displayName": "Ip", "type" : "QString", "inputType": "IPv4Address" + }, + { + "id": "2e8806cb-f6f3-4e9a-b6ea-0b35f75e61c5", + "name": "id", + "displayName": "Id", + "type" : "QString" } ], "stateTypes": [ @@ -42,16 +49,25 @@ "writable": true }, { - "displayName": "volume", + "displayName": "Mute", + "id": "3e11470d-a5b7-499c-be55-9b1b4fe5eedf", + "name": "mute", + "displayNameEvent": "Mute changed", + "displayNameAction": "Set mute", + "type": "bool", + "defaultValue": false, + "writable": true + }, + { + "displayName": "Volume", "id": "773636b9-304d-463a-8755-fc7488dc0ff3", "name": "volume", - "displayNameEvent": "volume changed", + "displayNameEvent": "Volume changed", "displayNameAction": "Set volume", "type": "int", - "unit": "Dezibel", "defaultValue": 0, "minValue": 0, - "maxValue": 80, + "maxValue": 100, "writable": true }, { @@ -84,6 +100,277 @@ "FVP" ], "defaultValue": "TUNER" + }, + { + "displayName": "Surround mode", + "id": "4f203bdd-691c-4384-a934-2d49a5448f0a", + "name": "surroundMode", + "displayNameEvent": "Surround mode changed", + "displayNameAction": "Set surround mode", + "type": "QString", + "writable": true, + "possibleValues": [ + "MOVIE", + "MUSIC", + "GAME", + "PURE DIRECT", + "DIRECT", + "STEREO", + "STANDARD", + "DOLBY DIGITAL", + "DTS SUROUND", + "MCH STEREO", + "ROCK ARENA", + "JAZZ CLUB", + "MONO MOVIE", + "MATRIX" + ], + "defaultValue": "MOVIE" + } + ], + "actionTypes": [ + { + "id": "4ae686d6-2307-40a0-bd38-2cd3a92342cc", + "displayName": "Increase volume", + "name": "increaseVolume", + "paramTypes": [ + { + "id": "765c7e2a-9eb6-46fc-a880-4e96c81f8d1e", + "displayName": "Step", + "name": "step", + "type": "int" + } + ] + }, + { + "id": "d3752c32-92e3-4396-8e2f-ab5e57c6cfb1", + "displayName": "Decrease volume", + "name": "decreaseVolume", + "paramTypes": [ + { + "id": "765c7e2a-9eb6-46fc-a880-4e96c81f8d1e", + "displayName": "Step", + "name": "step", + "type": "int" + } + ] + } + ] + }, + { + "id": "28bbf4c6-dfd8-4d9d-aa27-5daf2c25d63c", + "name": "heos", + "displayName": "Heos", + "createMethods": ["discovery"], + "interfaces": ["gateway"], + "paramTypes": [ + { + "id": "a54b98b4-b78f-41dd-a257-14425c6cf9ab", + "name": "ip", + "displayName": "IPv4 address", + "type" : "QString", + "inputType": "IPv4Address" + }, + { + "id": "f796664d-6cb7-4f29-9d05-771968d82a32", + "name": "serialNumber", + "displayName": "Serial number", + "type" : "QString" + }, + { + "id": "ab1a0be8-e3a5-4f95-b9b7-893de1ca4cf7", + "name": "modelName", + "displayName": "Model name", + "type" : "QString" + } + ], + "stateTypes": [ + { + "id": "4d1790bf-28c6-4c1f-8892-ba1a0ef140f5", + "name": "connected", + "displayName": "Connected", + "displayNameEvent": "Connected changed", + "defaultValue": false, + "type": "bool" + } + ] + }, + { + "id": "fce5247f-4c6d-408f-ac62-e5973dc6adfa", + "name": "heosPlayer", + "displayName": "Heos player", + "createMethods": ["auto"], + "interfaces": ["extendedmediacontroller", "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": "f796664d-6cb7-4f29-9d05-771968d82a32", + "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": "" + } + ], + "actionTypes": [ + { + "id": "a718f7e9-0b54-4403-b661-49f7b0d13085", + "name": "skipBack", + "displayName": "skip back" + }, + { + "id": "fe42d89f-aaad-4f33-a022-d80bdf3a7b19", + "name": "fastRewind", + "displayName": "fast rewind" + }, + { + "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": "60b62e88-c68b-463f-b328-2c5d67a71ca0", + "name": "fastForward", + "displayName": "fast forward" + }, + { + "id": "57697e9c-ce5e-4b8f-b42e-16662829ceb2", + "name": "skipNext", + "displayName": "skip next" } ] } diff --git a/denon/heos.cpp b/denon/heos.cpp new file mode 100644 index 00000000..b5d5a31d --- /dev/null +++ b/denon/heos.cpp @@ -0,0 +1,588 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * * + * Copyright (C) 2019 Bernhard Trinnes * + * * + * This file is part of nymea. * + * * + * This library is free software; you can redistribute it and/or * + * modify it under the terms of the GNU Lesser General Public * + * License as published by the Free Software Foundation; either * + * version 2.1 of the License, or (at your option) any later version. * + * * + * This library 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 library; If not, see * + * . * + * * + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +#include "heos.h" +#include "extern-plugininfo.h" +#include +#include +#include +#include +#include + +Heos::Heos(const QHostAddress &hostAddress, QObject *parent) : + QObject(parent), + m_hostAddress(hostAddress) +{ + m_socket = new QTcpSocket(this); + + connect(m_socket, &QTcpSocket::connected, this, &Heos::onConnected); + connect(m_socket, &QTcpSocket::disconnected, this, &Heos::onDisconnected); + connect(m_socket, &QTcpSocket::readyRead, this, &Heos::readData); + connect(m_socket, SIGNAL(error(QAbstractSocket::SocketError)), this, SLOT(onError(QAbstractSocket::SocketError))); +} + +Heos::~Heos() +{ + m_socket->close(); +} + +void Heos::connectHeos() +{ + if (m_socket->state() == QAbstractSocket::ConnectingState) { + return; + } + m_socket->connectToHost(m_hostAddress, 1255); +} + +/* + * SYSTEM COMMANDS + */ +void Heos::registerForChangeEvents(bool state) +{ + QByteArray query; + + if (state) { + query = "?enable=on"; + } else { + query = "?enable=off"; + } + QByteArray cmd = "heos://system/register_for_change_events" + query + "\r\n"; + qCDebug(dcDenon) << "Register for change events:" << cmd; + m_socket->write(cmd); +} + +void Heos::sendHeartbeat() +{ + QByteArray cmd = "heos://system/heart_beat\r\n"; + m_socket->write(cmd); +} + +void Heos::getUserAccount() +{ + QByteArray cmd = "heos://system/check_account\r\n"; + m_socket->write(cmd); +} + +void Heos::setUserAccount(QString userName, QString password) +{ + QByteArray cmd = "heos://system/sign_in?un=" + userName.toLocal8Bit() + "&pw=" + password.toLocal8Bit() + "\r\n"; + m_socket->write(cmd); +} + +void Heos::logoutUserAccount() +{ + QByteArray cmd = "heos://system/sign_out\r\n"; + m_socket->write(cmd); +} + +void Heos::rebootSpeaker() +{ + QByteArray cmd = "heos://system/reboot\r\n"; + m_socket->write(cmd); +} + +void Heos::prettifyJsonResponse(bool enable) +{ + QByteArray cmd = "heos://system/prettify_json_response?enable="; + if (enable) { + cmd.append("on=\r\n"); + } else { + cmd.append("off=\r\n"); + } + m_socket->write(cmd); +} + + +/* + * PLAYER COMMANDS + */ + +void Heos::getNowPlayingMedia(int playerId) +{ + QByteArray cmd = "heos://player/get_now_playing_media?pid=" + QVariant(playerId).toByteArray() + "\r\n"; + m_socket->write(cmd); +} + +HeosPlayer *Heos::getPlayer(int playerId) +{ + return m_heosPlayers.value(playerId); +} + +void Heos::getPlayers() +{ + QByteArray cmd = "heos://player/get_players\r\n"; + m_socket->write(cmd); +} + +void Heos::getVolume(int playerId) +{ + QByteArray cmd = "heos://player/get_volume?pid=" + QVariant(playerId).toByteArray() + "\r\n"; + m_socket->write(cmd); +} + +void Heos::setVolume(int playerId, int volume) +{ + QByteArray cmd = "heos://player/set_volume?pid=" + QVariant(playerId).toByteArray() + "&level=" + QVariant(volume).toByteArray() + "\r\n"; + qCDebug(dcDenon) << "Set volume:" << cmd; + m_socket->write(cmd); +} + +void Heos::getMute(int playerId) +{ + QByteArray cmd = "heos://player/get_mute?pid=" + QVariant(playerId).toByteArray() + "\r\n"; + m_socket->write(cmd); +} + +void Heos::setMute(int playerId, bool state) +{ + QByteArray stateQuery; + if(state) { + stateQuery = "&state=on"; + } else { + stateQuery = "&state=off"; + } + QByteArray cmd = "heos://player/set_mute?pid=" + QVariant(playerId).toByteArray() + stateQuery + "\r\n"; + qCDebug(dcDenon) << "Set mute:" << cmd; + m_socket->write(cmd); +} + +void Heos::setPlayerState(int playerId, PLAYER_STATE state) +{ + QByteArray playerStateQuery; + + if (state == PLAYER_STATE_PLAY){ + playerStateQuery = "&state=play"; + } else if (state == PLAYER_STATE_PAUSE){ + playerStateQuery = "&state=pause"; + } else if (state == PLAYER_STATE_STOP){ + playerStateQuery = "&state=stop"; + } + + QByteArray cmd = "heos://player/set_play_state?pid=" + QVariant(playerId).toByteArray() + playerStateQuery + "\r\n"; + qCDebug(dcDenon) << "Set play mode:" << cmd; + m_socket->write(cmd); +} + +void Heos::getPlayerState(int playerId) +{ + QByteArray cmd = "heos://player/get_play_state?pid=" + QVariant(playerId).toByteArray() + "\r\n"; + m_socket->write(cmd); +} + + +void Heos::setPlayMode(int playerId, REPEAT_MODE repeatMode, bool shuffle) +{ + QByteArray repeatModeQuery; + + if (repeatMode == REPEAT_MODE_OFF) { + repeatModeQuery = "&repeat=off"; + } else if (repeatMode == REPEAT_MODE_ONE) { + repeatModeQuery = "&repeat=on_one"; + } else if (repeatMode == REPEAT_MODE_ALL) { + repeatModeQuery = "&repeat=on_all"; + } + + QByteArray shuffleQuery; + if (shuffle) { + shuffleQuery = "&shuffle=on"; + } else { + shuffleQuery = "&shuffle=off"; + } + + QByteArray cmd = "heos://player/set_play_mode?pid=" + QVariant(playerId).toByteArray() + repeatModeQuery + shuffleQuery + "\r\n"; + qCDebug(dcDenon) << "Set play mode:" << cmd; + m_socket->write(cmd); +} + +void Heos::getPlayMode(int playerId) +{ + QByteArray cmd = "heos://player/get_play_mode?pid=" + QVariant(playerId).toByteArray() + "\r\n"; + m_socket->write(cmd); +} + +void Heos::getQueue(int playerId) +{ + QByteArray cmd = "heos://player/get_queue?pid=" + QVariant(playerId).toByteArray() + "\r\n"; + m_socket->write(cmd); +} + +/* + * GROUP COMMANDS + */ +void Heos::getGroups() +{ + QByteArray cmd = "heos://group/get_groups\r\n"; + m_socket->write(cmd); +} + +void Heos::getGroupInfo(int groupId) +{ + QByteArray cmd = "heos://group/get_group_info?gid=" + QVariant(groupId).toByteArray() + "\r\n"; + m_socket->write(cmd); +} + +void Heos::getGroupVolume(int groupId) +{ + QByteArray cmd = "heos://group/get_volume?gid=" + QVariant(groupId).toByteArray() + "\r\n"; + m_socket->write(cmd); +} + +void Heos::getGroupMute(int groupId) +{ + QByteArray cmd = "heos://group/get_mute?gid=" + QVariant(groupId).toByteArray() + "\r\n"; + m_socket->write(cmd); +} + +void Heos::playNext(int playerId) +{ + QByteArray cmd = "heos://player/play_next?pid=" + QVariant(playerId).toByteArray() + "\r\n"; + qCDebug(dcDenon) << "Play next:" << cmd; + m_socket->write(cmd); +} + +void Heos::playPrevious(int playerId) +{ + QByteArray cmd = "heos://player/play_previous?pid=" + QVariant(playerId).toByteArray() + "\r\n"; + qCDebug(dcDenon) << "Play previous:" << cmd; + m_socket->write(cmd); +} + +void Heos::volumeUp(int playerId, int step) +{ + QByteArray cmd = "heos://player/volume_up?pid=" + QVariant(playerId).toByteArray() + "&step=" + QVariant(step).toByteArray() + "\r\n"; + qCDebug(dcDenon) << "Volume up:" << cmd; + m_socket->write(cmd); +} + +void Heos::volumeDown(int playerId, int step) +{ + QByteArray cmd = "heos://player/volume_down?pid=" + QVariant(playerId).toByteArray() + "&step=" + QVariant(step).toByteArray() + "\r\n"; + qCDebug(dcDenon) << "Volume down:" << cmd; + m_socket->write(cmd); +} + +void Heos::clearQueue(int playerId) +{ + QByteArray cmd = "heos://player/clear_queue?pid=" + QVariant(playerId).toByteArray() + "\r\n"; + qCDebug(dcDenon) << "clear queue:" << cmd; + m_socket->write(cmd); +} + +void Heos::moveQueue(int playerId, int sourcQueueId, int destinationQueueId) +{ + QUrl url("player"); + url.setScheme("heos"); + url.setPath("move_queue_item"); + url.setQuery(QString("pid=%1").arg(playerId)); + url.setQuery(QString("sqid=%1").arg(sourcQueueId)); + url.setQuery(QString("dqid=%1").arg(destinationQueueId)); + qCDebug(dcDenon) << "moving queue:" << url; + m_socket->write(url.toEncoded()); +} + +void Heos::checkForFirmwareUpdate(int playerId) +{ + QByteArray cmd = "heos://player/check_update?pid=" + QVariant(playerId).toByteArray() + "\r\n"; + qCDebug(dcDenon) << "Check firmware update:" << cmd; + m_socket->write(cmd); +} + +void Heos::setGroupVolume(int groupId, bool volume) +{ + QByteArray cmd = "heos://group/set_volume?gid=" + QVariant(groupId).toByteArray() + "&level=" + QVariant(volume).toByteArray() + "\r\n"; + qCDebug(dcDenon) << "Volume up:" << cmd; + m_socket->write(cmd); +} + +void Heos::setGroupMute(int groupId, bool mute) +{ + QByteArray cmd = "heos://group/set_mute?gid=" + QVariant(groupId).toByteArray() + "&state="; + if (mute) { + cmd.append("on\r\n"); + } else { + cmd.append("off\r\n"); + } + m_socket->write(cmd); +} + +void Heos::toggleGroupMute(int groupId) +{ + QByteArray cmd = "heos://group/toggle_mute?gid=" + QVariant(groupId).toByteArray() + "\r\n"; + qCDebug(dcDenon) << "Volume up:" << cmd; + m_socket->write(cmd); +} + +void Heos::groupVolumeUp(int groupId, int step) +{ + QByteArray cmd = "heos://group/volume_up?pid=" + QVariant(groupId).toByteArray() + "&step=" + QVariant(step).toByteArray() + "\r\n"; + qCDebug(dcDenon) << "Group volume up:" << cmd; + m_socket->write(cmd); +} + +void Heos::groupVolumeDown(int groupId, int step) +{ + QByteArray cmd = "heos://group/volume_down?pid=" + QVariant(groupId).toByteArray() + "&step=" + QVariant(step).toByteArray() + "\r\n"; + qCDebug(dcDenon) << "Group volume up:" << cmd; + m_socket->write(cmd); +} + +void Heos::getMusicSources() +{ + QByteArray cmd = "heos://browse/get_music_sources\r\n"; + m_socket->write(cmd); +} + +void Heos::getSourceInfo(SOURCE_ID sourceId) +{ + QByteArray cmd = " heos://browse/get_source_info?sid=" + QVariant(sourceId).toByteArray() + "\r\n"; + qCDebug(dcDenon) << "Group volume up:" << cmd; + m_socket->write(cmd); +} + +void Heos::getSearchCriteria(SOURCE_ID sourceId) +{ + QByteArray cmd = "heos://browse/get_search_criteria?sid=" + QVariant(sourceId).toByteArray() + "\r\n"; + qCDebug(dcDenon) << "Group volume up:" << cmd; + m_socket->write(cmd); +} + +void Heos::browseSource(SOURCE_ID sourceId) +{ + QByteArray cmd = "heos://browse/browse?sid=" + QVariant(sourceId).toByteArray() + "\r\n"; + qCDebug(dcDenon) << "Group volume up:" << cmd; + m_socket->write(cmd); +} + +/* This command is used to perform the following actions: + * Create new group: Creates new group. First player id in the list is group leader. + * Adds or delete players from the group. First player id should be the group leader id. + * Ungroup all players in the group + * Ungroup players. Player id (pid) should be the group leader id. + */ +//void Heos::setGroup() +//{ +//} + + + +void Heos::onConnected() +{ + qCDebug(dcDenon()) << "connected successfully to" << m_hostAddress.toString(); + emit connectionStatusChanged(true); +} + +void Heos::onDisconnected() +{ + qCDebug(dcDenon()) << "Disconnected from" << m_hostAddress.toString() << "try reconnecting in 5 seconds"; + QTimer::singleShot(5000, this, [this](){ + connectHeos(); + }); + emit connectionStatusChanged(false); +} + +void Heos::onError(QAbstractSocket::SocketError socketError) +{ + qCWarning(dcDenon) << "socket error:" << socketError << m_socket->errorString(); +} + +void Heos::readData() +{ + int playerId = 0; + QByteArray data; + QJsonParseError error; + + while (m_socket->canReadLine()) { + data = m_socket->readLine(); + //qDebug(dcDenon) << data; + QJsonDocument jsonDoc = QJsonDocument::fromJson(data, &error); + if (error.error != QJsonParseError::NoError) { + qCWarning(dcDenon) << "failed to parse json :" << error.errorString(); + return; + } + + QVariantMap dataMap = jsonDoc.toVariant().toMap(); + if (dataMap.contains("heos")) { + QString command = dataMap.value("heos").toMap().value("command").toString(); + if (command.contains("register_for_change_events")) { + QString enabled = dataMap.value("heos").toMap().value("message").toString(); + if (enabled.contains("off")) { + qDebug(dcDenon) << "Events are disabled"; + m_eventRegistered = false; + } else { + qDebug(dcDenon) << "Events are enabled"; + m_eventRegistered = true; + } + + } else if (command.contains("get_players")) { + QVariantList payloadVariantList = jsonDoc.toVariant().toMap().value("payload").toList(); + + foreach (const QVariant &payloadEntryVariant, payloadVariantList) { + playerId = payloadEntryVariant.toMap().value("pid").toInt(); + if(!m_heosPlayers.contains(playerId)){ + QString serialNumber = payloadEntryVariant.toMap().value("serial").toString(); + QString name = payloadEntryVariant.toMap().value("name").toString(); + HeosPlayer *heosPlayer = new HeosPlayer(playerId, name, serialNumber, this); + m_heosPlayers.insert(playerId, heosPlayer); + emit playerDiscovered(heosPlayer); + } + } + + } else { + QUrlQuery message(dataMap.value("heos").toMap().value("message").toString()); + if (message.hasQueryItem("pid")) { + playerId = message.queryItemValue("pid").toInt(); + } + + if (command.contains("get_player_info")) { + //update heos player info + } + + if (command.contains("get_now_playing_media")) { + + QString artist = dataMap.value("payload").toMap().value("artist").toString(); + QString song = dataMap.value("payload").toMap().value("song").toString(); + QString artwork = dataMap.value("payload").toMap().value("image_url").toString(); + QString album = dataMap.value("payload").toMap().value("album").toString(); + SOURCE_ID sourceId = SOURCE_ID(dataMap.value("payload").toMap().value("sid").toInt()); + emit nowPlayingMediaStatusReceived(playerId, sourceId, artist, album, song, artwork); + } + + if (command.contains("get_play_state") || command.contains("set_play_state")) { + if (message.hasQueryItem("state")) { + PLAYER_STATE playState = PLAYER_STATE_STOP; + if (message.queryItemValue("state").contains("play")) { + playState = PLAYER_STATE_PLAY; + } else if (message.queryItemValue("state").contains("pause")) { + playState = PLAYER_STATE_PAUSE; + } else if (message.queryItemValue("state").contains("stop")) { + playState = PLAYER_STATE_STOP; + } + emit playStateReceived(playerId, playState); + } + } + + if (command.contains("get_volume") || command.contains("set_volume")) { + if (message.hasQueryItem("level")) { + int volume = message.queryItemValue("level").toInt(); + emit volumeStatusReceived(playerId, volume); + } + } + + if (command.contains("get_mute") || command.contains("set_mute")) { + if (message.hasQueryItem("state")) { + QString state = message.queryItemValue("state"); + if (state.contains("on")) { + emit muteStatusReceived(playerId, true); + } else { + emit muteStatusReceived(playerId, false); + } + } + } + + if (command.contains("get_play_mode") || command.contains("set_play_mode")) { + if (message.hasQueryItem("shuffle") && message.hasQueryItem("repeat")) { + bool shuffle; + if (message.queryItemValue("shuffle").contains("on")){ + shuffle = true; + } else { + shuffle = false; + } + emit shuffleModeReceived(playerId, shuffle); + + REPEAT_MODE repeatMode = REPEAT_MODE_OFF; + if (message.queryItemValue("repeat").contains("on_all")){ + repeatMode = REPEAT_MODE_ALL; + } else if (message.queryItemValue("repeat").contains("on_one")){ + repeatMode = REPEAT_MODE_ONE; + } else if (message.queryItemValue("repeat").contains("off")){ + repeatMode = REPEAT_MODE_OFF; + } + emit repeatModeReceived(playerId, repeatMode); + } + } + + if (command.contains("player_state_changed")) { + if (message.hasQueryItem("state")) { + PLAYER_STATE playState = PLAYER_STATE_STOP; + if (message.queryItemValue("state").contains("play")) { + playState = PLAYER_STATE_PLAY; + } else if (message.queryItemValue("state").contains("pause")) { + playState = PLAYER_STATE_PAUSE; + } else if (message.queryItemValue("state").contains("stop")) { + playState = PLAYER_STATE_STOP; + } + emit playStateReceived(playerId, playState); + } + } + + if (command.contains("player_volume_changed")) { + qDebug() << "Volume Changed"; + if (message.hasQueryItem("level")) { + int volume = message.queryItemValue("level").toInt(); + emit volumeStatusReceived(playerId, volume); + } + if (message.hasQueryItem("mute")) { + bool mute; + if (message.queryItemValue("mute").contains("on")) { + mute = true; + } else { + mute = false; + } + emit muteStatusReceived(playerId, mute); + } + } + + if (command.contains("repeat_mode_changed")) { + + if (message.hasQueryItem("repeat")) { + REPEAT_MODE repeatMode = REPEAT_MODE_OFF; + if (message.queryItemValue("repeat").contains("on_all")){ + repeatMode = REPEAT_MODE_ALL; + } else if (message.queryItemValue("repeat").contains("on_one")){ + repeatMode = REPEAT_MODE_ONE; + } else if (message.queryItemValue("repeat").contains("off")){ + repeatMode = REPEAT_MODE_OFF; + } + emit repeatModeReceived(playerId, repeatMode); + } + } + + if (command.contains("shuffle_mode_changed")) { + + if (message.hasQueryItem("shuffle")) { + bool shuffle; + if (message.queryItemValue("shuffle").contains("on")){ + shuffle = true; + } else { + shuffle = false; + } + emit shuffleModeReceived(playerId, shuffle); + } + } + + if (command.contains("player_now_playing_changed")) { + getNowPlayingMedia(playerId); + } + } + } + } +} diff --git a/denon/heos.h b/denon/heos.h new file mode 100644 index 00000000..59e7c052 --- /dev/null +++ b/denon/heos.h @@ -0,0 +1,124 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * * + * Copyright (C) 2019 Bernhard Trinnes * + * * + * This file is part of nymea. * + * * + * This library is free software; you can redistribute it and/or * + * modify it under the terms of the GNU Lesser General Public * + * License as published by the Free Software Foundation; either * + * version 2.1 of the License, or (at your option) any later version. * + * * + * This library 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 library; If not, see * + * . * + * * + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +#ifndef HEOS_H +#define HEOS_H + +#include +#include +#include + +#include "heosplayer.h" +#include "heostypes.h" + +class Heos : public QObject +{ + Q_OBJECT +public: + + explicit Heos(const QHostAddress &hostAddress, QObject *parent = nullptr); + ~Heos(); + + void connectHeos(); + void setAddress(QHostAddress address); + QHostAddress getAddress(); + HeosPlayer *getPlayer(int playerId); + + // Heos system commands + void registerForChangeEvents(bool state); //By default HEOS speaker does not send Change events. Controller needs to send this command with enable=on when it is ready to receive unsolicit responses from CLI. Please refer to "Driver Initialization" section regarding when to register for change events. + void sendHeartbeat(); + void getUserAccount(); //returns current user name in its message field if the user is currently singed in. + void setUserAccount(QString userName, QString password); + void logoutUserAccount(); + void rebootSpeaker(); //Using this command controllers can reboot HEOS device. This command can only be used to reboot the HEOS device to which the controller is connected through CLI port. + void prettifyJsonResponse(bool enable); //Helper command to prettify JSON response when user is running CLI controller through telnet. + + //Player Get Calls + void getPlayers(); //get a list of players associated with this heos master + void getPlayerState(int playerId); + void getVolume(int playerId); + void getNowPlayingMedia(int playerId); + void getMute(int playerId); + void getPlayMode(int playerId); + void getQueue(int playerId); + + //Group Get Calls + void getGroups(); + void getGroupInfo(int groupId); + void getGroupVolume(int groupId); + void getGroupMute(int groupId); + + //Player Set Calls + void setPlayerState(int playerId, PLAYER_STATE state); + void setVolume(int playerId, int volume); //Player volume level 0 to 100 + void setMute(int playerId, bool mute); + void setPlayMode(int playerId, REPEAT_MODE repeatMode, bool shuffle); //shuffle and repead mode + void playNext(int playerId); + void playPrevious(int playerId); + void volumeUp(int playerId, int step = 5); //steps 0-10 + void volumeDown(int playerId, int step = 5); //steps 0-10 + void clearQueue(int playerId); + void moveQueue(int playerId, int sourcQueueId, int destinationQueueId); + void checkForFirmwareUpdate(int playerId); + + //Group Set Calls + void setGroupVolume(int groupId, bool volume); + void setGroupMute(int groupId, bool mute); + void toggleGroupMute(int groupId); + void groupVolumeUp(int groupId, int step = 5); + void groupVolumeDown(int groupId, int step = 5); + + //Browse Get Commands + void getMusicSources(); + void getSourceInfo(SOURCE_ID sourceId); + void getSearchCriteria(SOURCE_ID sourceId); + void browseSource(SOURCE_ID sourceId); + //void search(); + + +private: + bool m_eventRegistered = false; + QHostAddress m_hostAddress; + QTcpSocket *m_socket = nullptr; + QHash m_heosPlayers; + void setConnected(const bool &connected); + +signals: + void playerDiscovered(HeosPlayer *heosPlayer); + void connectionStatusChanged(bool status); + + void playStateReceived(int playerId, PLAYER_STATE state); + void shuffleModeReceived(int playerId, bool shuffle); + void repeatModeReceived(int playerId, REPEAT_MODE repeatMode); + void muteStatusReceived(int playerId, bool mute); + void volumeStatusReceived(int playerId, int volume); + void nowPlayingMediaStatusReceived(int playerId, SOURCE_ID source, QString artist, QString album, QString Song, QString artwork); + +private slots: + void onConnected(); + void onDisconnected(); + void onError(QAbstractSocket::SocketError socketError); + void readData(); +}; + + +#endif // HEOS_H diff --git a/denon/heosplayer.cpp b/denon/heosplayer.cpp new file mode 100644 index 00000000..51ed1bcb --- /dev/null +++ b/denon/heosplayer.cpp @@ -0,0 +1,95 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * * + * Copyright (C) 2019 Bernhard Trinnes * + * * + * This file is part of nymea. * + * * + * This library is free software; you can redistribute it and/or * + * modify it under the terms of the GNU Lesser General Public * + * License as published by the Free Software Foundation; either * + * version 2.1 of the License, or (at your option) any later version. * + * * + * This library 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 library; If not, see * + * . * + * * + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +#include "heosplayer.h" + +HeosPlayer::HeosPlayer(int playerId, QObject *parent) : + QObject(parent), + m_playerId(playerId) +{ + +} + +HeosPlayer::HeosPlayer(int playerId, QString name, QString serialNumber, QObject *parent) : + QObject(parent), + m_playerId(playerId), + m_serialNumber(serialNumber), + m_name(name) +{ +} + +QString HeosPlayer::name() +{ + return m_name; +} + +void HeosPlayer::setName(QString name) +{ + m_name = name; +} + +int HeosPlayer::playerId() +{ + return m_playerId; +} + +int HeosPlayer::groupId() +{ + return m_groupId; +} + +void HeosPlayer::setGroupId(int groupId) +{ + m_groupId = groupId; +} + +QString HeosPlayer::playerModel() +{ + return m_playerModel; +} + +QString HeosPlayer::playerVersion() +{ + return m_playerVersion; +} + +QString HeosPlayer::network() +{ + return m_network; +} + +QString HeosPlayer::serialNumber() +{ + return m_serialNumber; +} + +QString HeosPlayer::lineOut() +{ + return m_lineOut; +} + +QString HeosPlayer::control() +{ + return m_control; +} + + diff --git a/denon/heosplayer.h b/denon/heosplayer.h new file mode 100644 index 00000000..aa7a27ef --- /dev/null +++ b/denon/heosplayer.h @@ -0,0 +1,60 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * * + * Copyright (C) 2019 Bernhard Trinnes * + * * + * This file is part of nymea. * + * * + * This library is free software; you can redistribute it and/or * + * modify it under the terms of the GNU Lesser General Public * + * License as published by the Free Software Foundation; either * + * version 2.1 of the License, or (at your option) any later version. * + * * + * This library 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 library; If not, see * + * . * + * * + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +#ifndef HEOSPLAYER_H +#define HEOSPLAYER_H + +#include + +class HeosPlayer : public QObject +{ + Q_OBJECT +public: + explicit HeosPlayer(int playerId, QObject *parent = nullptr); + explicit HeosPlayer(int playerId, QString name, QString serialNumber, QObject *parent = nullptr); + + QString name(); + void setName(QString name); + int playerId(); + int groupId(); + void setGroupId(int groupId); + QString playerModel(); + QString playerVersion(); + QString network(); + QString serialNumber(); + QString lineOut(); + QString control(); + +private: + int m_playerId; + int m_groupId; + QString m_serialNumber; + QString m_name; + QString m_lineOut; + QString m_network; + QString m_playerModel; + QString m_playerVersion; + QString m_control; + +}; + +#endif // HEOSPLAYER_H diff --git a/denon/heostypes.h b/denon/heostypes.h new file mode 100644 index 00000000..ba58db89 --- /dev/null +++ b/denon/heostypes.h @@ -0,0 +1,137 @@ +#ifndef HEOSTYPES_H +#define HEOSTYPES_H + +#include "extern-plugininfo.h" + +enum NETWORK_TYPE { + NETWORK_TYPE_WIRED, + NETWORK_TYPE_WIFI +} ; + +enum LINEOUT_LEVEL_TYPE { + LINEOUT_LEVEL_TYPE_VARIABLE = 1, + LINEOUT_LEVEL_TYP_FIXED = 2 +}; + +enum CONTROL_TYPE { + CONTROL_TYPE_NONE = 1, + CONTROL_TYPE_IR = 2, + CONTROL_TYPE_TRIGGER = 3, + CONTROL_TYPE_NETWORK = 4 +}; + +enum PLAYER_STATE { + PLAYER_STATE_PLAY, + PLAYER_STATE_PAUSE, + PLAYER_STATE_STOP +}; + +enum NOW_PLAYING_OPTIONS { + NOW_PLAYING_OPTION_THUMBS_UP = 11, + NOW_PLAYING_OPTION_THUMBS_DOWN = 12, + NOW_PLAYING_OPTION_ADD_STATION_TO_HEOS_FAVOURITES = 19 +}; + +enum REPEAT_MODE { + REPEAT_MODE_OFF, + REPEAT_MODE_ONE, + REPEAT_MODE_ALL +}; + +enum PLAYER_ROLE { + PLAYER_ROLE_LEADER, + PLAYER_ROLE_MEMBER +}; + +enum BROWSE_OPTION { + BROWSE_OPTION_ADD_TRACK_TO_LIBRARY = 1, + BROWSE_OPTION_ADD_ALBUM_TO_LIBRARY = 2, + BROWSE_OPTION_ADD_STATION_TO_LIBRARY = 3, + BROWSE_OPTION_ADD_PLAYLIST_TO_LIBRARY = 4, + BROWSE_OPTION_REMOVE_TRACK_FROM_LIBRARY = 5, + BROWSE_OPTION_REMOVE_ALBUM_FROM_LIBRARY = 6, + BROWSE_OPTION_REMOVE_STATION_FROM_LIBRARY = 7, + BROWSE_OPTION_REMOVE_PLAYLIST_FROM_LIBRARY = 8, + BROWSE_OPTION_CREATE_NEW_STATION = 13, + BROWSE_OPTION_ADD_HEOS_FAVORITES = 19 +}; + +enum SEARCH_CRITERIA { // criteria id returned by 'get_search_criteria' command + SEARCH_CRITERIA_ARTIST, + SEARCH_CRITERIA_ALBUM, + SEARCH_CRITERIA_SONG, + SEARCH_CRITERIA_STATION +}; + +enum SOURCE_ID { + SOURCE_ID_PANDORA = 1, + SOURCE_ID_RHAPSODY, + SOURCE_ID_TUNEIN, + SOURCE_ID_SPOTIFY, + SOURCE_ID_DEEZER, + SOURCE_ID_NAPSTER, + SOURCE_ID_IHEARTRADIO, + SOURCE_ID_SIRIUS_XM, + SOURCE_ID_SOUNDCLOUD, + SOURCE_ID_TIDAL, + SOURCE_ID_FUTURE_SERVICE_1, + SOURCE_ID_RDIO, + SOURCE_ID_AMAZON_MUSIC, + SOURCE_ID_FUTURE_SERVICE_2, + SOURCE_ID_MOODMIX, + SOURCE_ID_JUKE, + SOURCE_ID_FUTURE_SERVICE_3, + SOURCE_ID_QQMUSIC = 18, + SOURCE_ID_LOCAL_MEDIA = 1024, + SOURCE_ID_HEOS_PLAYLIST = 1025, + SOURCE_ID_HEOS_HISTORY = 1026, + SOURCE_ID_HEOS_AUX = 1027, + SOURCE_ID_HEOS_FAVORITES = 1028 +}; + +struct SearchObject { + int sourceId; //Source id returned by 'get_music_sources' command + QString searchString; //String for search limited to 128 unicode characters and may contain '*' for wildcard if supported by search criteria id + SEARCH_CRITERIA searchCriteria; //Search criteria id returned by 'get_search_criteria' command + int count; //Total number of items available in the container. NOTE: count value of '0' indicates unknown container size. Controllers needs to query until the return payload is empty (returned attribute is 0). + int range; //Range is start and end record index to return. Range parameter is optional. Omitting range parameter returns all records up to a maximum of 50/100 records per response. The default maximum number of records depend on the service type. + int returned; //Number of items returned in current response +}; + +struct MusicSourceObject { + QString name; + QString image_url; + QString type; + int sourceId; + bool available; + QString serviceUsername; +}; + +struct PlayerObject { + QString name; + int playerId; + PLAYER_ROLE role; +}; + +struct GroupObject { + QString name; + int groupId; + QList role; +}; + +struct SourceContainersObject { + int sourceId; + int containerId; + int range; + int count; +}; + +struct heosPlayer { + +}; + +struct heosGroup { + +}; + +#endif // HEOSTYPES_H