diff --git a/denon/denon.pro b/denon/denon.pro index 6a613690..41adb02f 100644 --- a/denon/denon.pro +++ b/denon/denon.pro @@ -6,8 +6,12 @@ TARGET = $$qtLibraryTarget(nymea_deviceplugindenon) SOURCES += \ deviceplugindenon.cpp \ - denonconnection.cpp + denonconnection.cpp \ + heos.cpp \ + heosplayer.cpp \ HEADERS += \ deviceplugindenon.h \ - denonconnection.h + denonconnection.h \ + heos.h \ + heosplayer.h \ diff --git a/denon/deviceplugindenon.cpp b/denon/deviceplugindenon.cpp index f3a7ec2d..91cfcf90 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,89 +21,166 @@ * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ +/*! + \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 "network/upnp/upnpdiscovery.h" +#include "network/upnp/upnpdiscoveryreply.h" +#include "network/avahi/qtavahiservicebrowser.h" + +#include +#include +#include DevicePluginDenon::DevicePluginDenon() { - } -DevicePluginDenon::~DevicePluginDenon() +DeviceManager::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) { + QtAvahiServiceBrowser *avahiBrowser = hardwareManager()->avahiBrowser(); + connect(avahiBrowser, &QtAvahiServiceBrowser::serviceEntryAdded, this, &DevicePluginDenon::onAvahiEntryAdded); + return DeviceManager::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 DeviceManager::DeviceErrorAsync; + } + return DeviceManager::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 DeviceManager::DeviceSetupStatusFailure; + } + + DenonConnection *denonConnection = new DenonConnection(address, 23, this); + connect(denonConnection, &DenonConnection::connectionStatusChanged, this, &DevicePluginDenon::onAVRConnectionChanged); + connect(denonConnection, &DenonConnection::socketErrorOccured, this, &DevicePluginDenon::onAVRSocketError); + connect(denonConnection, &DenonConnection::dataReady, this, &DevicePluginDenon::onAVRDataReceived); + + m_asyncSetups.append(denonConnection); + denonConnection->connectDenon(); + m_denonConnections.insert(device, denonConnection); + return DeviceManager::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); - return Device::DeviceSetupStatusAsync; + 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); + + heos->connectHeos(); + m_heos.insert(device, heos); + return DeviceManager::DeviceSetupStatusAsync; + } + + if (device->deviceClassId() == heosPlayerDeviceClassId) { + return DeviceManager::DeviceSetupStatusSuccess; + } + + return DeviceManager::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) { + DenonConnection *denonConnection = m_denonConnections.value(device); + m_denonConnections.remove(device); + denonConnection->disconnectDenon(); + denonConnection->deleteLater(); + } + + if (device->deviceClassId() == heosDeviceClassId) { + if (m_denonConnections.contains(device)) { + DenonConnection *denonConnection = m_denonConnections.value(device); + m_denonConnections.remove(device); + denonConnection->disconnectDenon(); + denonConnection->deleteLater(); + } + } + + if (device->deviceClassId() == heosPlayerDeviceClassId) { + } + + 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) { + DenonConnection *denonConnection = m_denonConnections.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); + denonConnection->sendData(cmd); } else { QByteArray cmd = "PWSTANDBY\r"; qCDebug(dcDenon) << "Execute power: " << action.id() << cmd; - m_denonConnection->sendData(cmd); + denonConnection->sendData(cmd); } return Device::DeviceErrorNoError; @@ -114,7 +191,7 @@ Device::DeviceError DevicePluginDenon::executeAction(Device *device, const Actio QByteArray cmd = "MV" + vol + "\r"; qCDebug(dcDenon) << "Execute volume" << action.id() << cmd; - m_denonConnection->sendData(cmd); + denonConnection->sendData(cmd); return Device::DeviceErrorNoError; @@ -125,134 +202,454 @@ Device::DeviceError DevicePluginDenon::executeAction(Device *device, const Actio QByteArray cmd = "SI" + channel + "\r"; qCDebug(dcDenon) << "Change to channel:" << cmd; - m_denonConnection->sendData(cmd); + denonConnection->sendData(cmd); - return Device::DeviceErrorNoError; + return DeviceManager::DeviceErrorNoError; + } else if (action.actionTypeId() == AVRX1000IncreaseVolumeActionTypeId) { + QByteArray cmd = "MVUP\r"; + qCDebug(dcDenon) << "Execute volume increase" << action.id() << cmd; + denonConnection->sendData(cmd); + + return DeviceManager::DeviceErrorNoError; + } else if (action.actionTypeId() == AVRX1000DecreaseVolumeActionTypeId) { + QByteArray cmd = "MVDOWN\r"; + qCDebug(dcDenon) << "Execute volume decrease" << action.id() << cmd; + denonConnection->sendData(cmd); + return DeviceManager::DeviceErrorNoError; } - return Device::DeviceErrorActionTypeNotFound; + + + return DeviceManager::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 DeviceManager::DeviceErrorNoError; + } + + if (action.actionTypeId() == heosPlayerMuteActionTypeId) { + bool mute = action.param(heosPlayerMuteActionMuteParamTypeId).value().toBool(); + heos->setMute(playerId, mute); + return DeviceManager::DeviceErrorNoError; + } + + if (action.actionTypeId() == heosPlayerPlaybackStatusActionTypeId) { + QString playbackStatus = action.param(heosPlayerPlaybackStatusActionPlaybackStatusParamTypeId).value().toString(); + if (playbackStatus == "playing") { + heos->setPlayerState(playerId, Heos::HeosPlayerState::Play); + } else if (playbackStatus == "stopping") { + heos->setPlayerState(playerId, Heos::HeosPlayerState::Stop); + } else if (playbackStatus == "pausing") { + heos->setPlayerState(playerId, Heos::HeosPlayerState::Pause); + } + + return DeviceManager::DeviceErrorNoError; + } + + if (action.actionTypeId() == heosPlayerShuffleActionTypeId) { + bool shuffle = action.param(heosPlayerShuffleActionShuffleParamTypeId).value().toBool(); + Heos::HeosRepeatMode repeatMode; + repeatMode = Heos::HeosRepeatMode::Off; + heos->setPlayMode(playerId, repeatMode, shuffle); + + return DeviceManager::DeviceErrorNoError; + } + + if (action.actionTypeId() == heosPlayerSkipBackActionTypeId) { + heos->playPrevious(playerId); + return DeviceManager::DeviceErrorNoError; + } + + if (action.actionTypeId() == heosPlayerFastRewindActionTypeId) { + + return DeviceManager::DeviceErrorActionTypeNotFound; + } + + if (action.actionTypeId() == heosPlayerStopActionTypeId) { + heos->setPlayerState(playerId, Heos::HeosPlayerState::Stop); + return DeviceManager::DeviceErrorNoError; + } + + if (action.actionTypeId() == heosPlayerPlayActionTypeId) { + heos->setPlayerState(playerId, Heos::HeosPlayerState::Play); + return DeviceManager::DeviceErrorNoError; + } + + if (action.actionTypeId() == heosPlayerPauseActionTypeId) { + + heos->setPlayerState(playerId, Heos::HeosPlayerState::Pause); + return DeviceManager::DeviceErrorNoError; + } + + if (action.actionTypeId() == heosPlayerFastForwardActionTypeId) { + + return DeviceManager::DeviceErrorActionTypeNotFound; + } + + if (action.actionTypeId() == heosPlayerSkipNextActionTypeId) { + heos->playNext(playerId); + return DeviceManager::DeviceErrorNoError; + } + + return DeviceManager::DeviceErrorActionTypeNotFound; } return Device::DeviceErrorDeviceClassNotFound; } +void DevicePluginDenon::postSetupDevice(Device *device) +{ + if (device->deviceClassId() == AVRX1000DeviceClassId) { + + } + + if (device->deviceClassId() == heosDeviceClassId) { + Heos *heos = m_heos.value(device); + heos->getPlayers(); + device->setStateValue(heosConnectedStateTypeId, heos->connected()); + } + + if (device->deviceClassId() == heosPlayerDeviceClassId) { + + 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); + device->setStateValue(heosPlayerConnectedStateTypeId, heos->connected()); + } +} + +void DevicePluginDenon::startMonitoringAutoDevices() +{ + +} + 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(DenonConnection *denonConnection, m_denonConnections.values()) { + if (!denonConnection->connected()) { + denonConnection->connectDenon(); + } + Device *device = m_denonConnections.key(denonConnection); + if (device->deviceClassId() == AVRX1000DeviceClassId) { + denonConnection->sendData("PW?\rSI?\rMV?\r"); } } - // Set connection status - m_device->setStateValue(AVRX1000ConnectedStateTypeId, m_denonConnection->connected()); + foreach(Device *device, myDevices()) { + + if (device->deviceClassId() == heosDeviceClassId) { + Heos *heos = m_heos.value(device); + if (!heos->connected()) { + heos->connectHeos(); + } + device->setStateValue(heosConnectedStateTypeId, heos->connected()); + heos->getPlayers(); + heos->registerForChangeEvents(true); + } + + + if (device->deviceClassId() == heosPlayerDeviceClassId) { + 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::onDataReceived(const QByteArray &data) +void DevicePluginDenon::onAVRConnectionChanged() { + DenonConnection *denonConnection = static_cast(sender()); + Device *device = m_denonConnections.key(denonConnection); + + if (device->deviceClassId() == AVRX1000DeviceClassId) { + // if the device is connected + if (denonConnection->connected()) { + // and from the first setup + if (m_asyncSetups.contains(denonConnection)) { + m_asyncSetups.removeAll(denonConnection); + denonConnection->sendData("PW?\rSI?\rMV?\r"); + emit deviceSetupFinished(device, DeviceManager::DeviceSetupStatusSuccess); + } + } + device->setStateValue(AVRX1000ConnectedStateTypeId, denonConnection->connected()); + } +} + +void DevicePluginDenon::onAVRDataReceived(const QByteArray &data) +{ + DenonConnection *denonConnection = static_cast(sender()); + Device *device = m_denonConnections.key(denonConnection); qCDebug(dcDenon) << "Data received" << data; - // if there is no device, return - if (m_device.isNull()) - return; + if (device->deviceClassId() == AVRX1000DeviceClassId) { + if (data.contains("MV") && !data.contains("MAX")){ + int index = data.indexOf("MV"); + int vol = data.mid(index+2, 2).toInt(); - 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"; + qCDebug(dcDenon) << "Update volume:" << vol; + device->setStateValue(AVRX1000VolumeStateTypeId, vol); } - qCDebug(dcDenon) << "Update channel:" << cmd; - m_device->setStateValue(AVRX1000ChannelStateTypeId, cmd); - } + if (data.contains("SI")) { + QString cmd = NULL; + 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 (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); + qCDebug(dcDenon) << "Update channel:" << cmd; + device->setStateValue(AVRX1000ChannelStateTypeId, cmd); + } + + if (data.contains("PWON")) { + qCDebug(dcDenon) << "Update power on"; + device->setStateValue(AVRX1000PowerStateTypeId, true); + } else if (data.contains("PWSTANDBY")) { + qCDebug(dcDenon) << "Update power off"; + device->setStateValue(AVRX1000PowerStateTypeId, false); + } } } -void DevicePluginDenon::onSocketError() + +void DevicePluginDenon::onAVRSocketError() { - // if there is no device, return - if (m_device.isNull()) - return; + DenonConnection *denonConnection = static_cast(sender()); + Device *device = m_denonConnections.key(denonConnection); + if (device->deviceClassId() == AVRX1000DeviceClassId) { - // 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(); + // Check if setup running for this device + if (m_asyncSetups.contains(denonConnection)) { + m_asyncSetups.removeAll(denonConnection); + qCWarning(dcDenon()) << "Could not add device. The setup failed."; + emit deviceSetupFinished(device, DeviceManager::DeviceSetupStatusFailure); + // Delete the connection, the device will not be added and + // the connection will be created in the next setup + denonConnection->deleteLater(); + m_denonConnections.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 serialNumbers; + foreach (Device *device, myDevices()) { + if (device->deviceClassId() == heosDeviceClassId){ + serialNumbers.append(device->paramValue(heosDeviceSerialNumberParamTypeId).toString()); + } + } + + QList deviceDescriptors; + foreach (const UpnpDeviceDescriptor &upnpDevice, reply->deviceDescriptors()) { + qCDebug(dcDenon) << "UPnP device found:" << upnpDevice.modelDescription() << upnpDevice.friendlyName() << upnpDevice.hostAddress().toString() << upnpDevice.modelName() << upnpDevice.manufacturer() << upnpDevice.serialNumber() << upnpDevice.deviceType() << upnpDevice.location(); + if (upnpDevice.modelName().contains("HEOS")) { + //check if not already addded + QString serialNumber = upnpDevice.serialNumber(); + if ((!serialNumbers.contains(serialNumber)) && (serialNumber !=("0000001"))) { + DeviceDescriptor descriptor(heosDeviceClassId, upnpDevice.modelName(), serialNumber); + ParamList params; + params.append(Param(heosDeviceModelNameParamTypeId, upnpDevice.modelName())); + params.append(Param(heosDeviceIpParamTypeId, upnpDevice.hostAddress().toString())); + params.append(Param(heosDeviceSerialNumberParamTypeId, serialNumber)); + descriptor.setParams(params); + deviceDescriptors.append(descriptor); + serialNumbers.append(serialNumber); + } + } + } + + emit devicesDiscovered(heosDeviceClassId, deviceDescriptors); +} + +void DevicePluginDenon::onAvahiEntryAdded() { + qCDebug(dcDenon) << "Avahi entry added"; +} + +void DevicePluginDenon::onHeosConnectionChanged() +{ + Heos *heos = static_cast(sender()); + heos->registerForChangeEvents(true); + Device *device = m_heos.key(heos); + if (!device->setupComplete() && heos->connected()) { + emit deviceSetupFinished(device, DeviceManager::DeviceSetupStatusSuccess); + } +} + +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, Heos::HeosPlayerState state) +{ + foreach (Device *device, myDevices()) { + if(device->deviceClassId() == heosPlayerDeviceClassId) { + if (device->paramValue(heosPlayerDevicePlayerIdParamTypeId).toInt() == playerId) { + if (state == Heos::HeosPlayerState::Pause) { + device->setStateValue(heosPlayerPlaybackStatusStateTypeId, "Paused"); + } else if (state == Heos::HeosPlayerState::Play) { + device->setStateValue(heosPlayerPlaybackStatusStateTypeId, "Playing"); + } else if (state == Heos::HeosPlayerState::Stop) { + device->setStateValue(heosPlayerPlaybackStatusStateTypeId, "Stopped"); + } + } + } + } +} + + +void DevicePluginDenon::onHeosRepeatModeReceived(int playerId, Heos::HeosRepeatMode repeatMode) +{ + foreach (Device *device, myDevices()) { + if(device->deviceClassId() == heosPlayerDeviceClassId) { + if (device->paramValue(heosPlayerDevicePlayerIdParamTypeId).toInt() == playerId) { + if (repeatMode == Heos::HeosRepeatMode::All) { + device->setStateValue(heosPlayerRepeatStateTypeId, "All"); + } else if (repeatMode == Heos::HeosRepeatMode::One) { + device->setStateValue(heosPlayerRepeatStateTypeId, "One"); + } else if (repeatMode == Heos::HeosRepeatMode::Off) { + device->setStateValue(heosPlayerRepeatStateTypeId, "None"); + } + } + } + } +} + +void DevicePluginDenon::onHeosShuffleModeReceived(int playerId, bool shuffle) +{ + foreach (Device *device, myDevices()) { + if(device->deviceClassId() == heosPlayerDeviceClassId) { + if (device->paramValue(heosPlayerDevicePlayerIdParamTypeId).toInt() == playerId) { + + if (shuffle) { + device->setStateValue(heosPlayerMuteStateTypeId, true); + } else { + device->setStateValue(heosPlayerMuteStateTypeId, false); + } + } + } + } +} + +void DevicePluginDenon::onHeosMuteStatusReceived(int playerId, bool mute) +{ + foreach (Device *device, myDevices()) { + if(device->deviceClassId() == heosPlayerDeviceClassId) { + if (device->paramValue(heosPlayerDevicePlayerIdParamTypeId).toInt() == playerId) { + device->setStateValue(heosPlayerMuteStateTypeId, mute); + } + } + } +} + +void DevicePluginDenon::onHeosVolumeStatusReceived(int playerId, int volume) +{ + foreach (Device *device, myDevices()) { + if(device->deviceClassId() == heosPlayerDeviceClassId) { + if (device->paramValue(heosPlayerDevicePlayerIdParamTypeId).toInt() == playerId) { + device->setStateValue(heosPlayerVolumeStateTypeId, volume); + } + } + } +} + +void DevicePluginDenon::onHeosNowPlayingMediaStatusReceived(int playerId, QString source, QString artist, QString album, QString song, QString artwork) +{ + foreach (Device *device, myDevices()) { + if(device->deviceClassId() == heosPlayerDeviceClassId) { + if (device->paramValue(heosPlayerDevicePlayerIdParamTypeId).toInt() == playerId) { + device->setStateValue(heosPlayerArtistStateTypeId, artist); + device->setStateValue(heosPlayerTitleStateTypeId, song); + device->setStateValue(heosPlayerArtworkStateTypeId, artwork); + device->setStateValue(heosPlayerCollectionStateTypeId, album); + device->setStateValue(heosPlayerSourceStateTypeId, source); + } + } + } +} diff --git a/denon/deviceplugindenon.h b/denon/deviceplugindenon.h index e144af27..a986ddf1 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. * * * @@ -26,7 +26,6 @@ #include "devices/deviceplugin.h" - #include #include #include @@ -35,6 +34,8 @@ #include "plugintimer.h" #include "denonconnection.h" +#include "heos.h" +#include class DevicePluginDenon : public DevicePlugin { @@ -45,27 +46,43 @@ class DevicePluginDenon : public DevicePlugin public: explicit DevicePluginDenon(); - ~DevicePluginDenon(); - void init() override; - Device::DeviceSetupStatus setupDevice(Device *device) override; + void startMonitoringAutoDevices() override; + DeviceManager::DeviceError discoverDevices(const DeviceClassId &deviceClassId, const ParamList ¶ms) override; + DeviceManager::DeviceSetupStatus setupDevice(Device *device) override; + void postSetupDevice(Device * device) override; + DeviceManager::DeviceError executeAction(Device *device, const Action &action) override; void deviceRemoved(Device *device) override; - Device::DeviceError executeAction(Device *device, const Action &action) override; private: PluginTimer *m_pluginTimer = nullptr; - QPointer m_device; - QPointer m_denonConnection; + QHash m_denonConnections; + QHash m_heos; + QList m_asyncSetups; - QHash m_asyncActions; - QHash m_asyncActionReplies; + QHash m_playerIds; + QHash m_discoveredPlayerIds; + QHash m_asyncActions; + private slots: void onPluginTimer(); - void onConnectionChanged(); - void onDataReceived(const QByteArray &data); - void onSocketError(); + void onAVRConnectionChanged(); + void onAVRDataReceived(const QByteArray &data); + void onAVRSocketError(); + + void onUpnpDiscoveryFinished(); + void onAvahiEntryAdded(); + + void onHeosConnectionChanged(); + void onHeosPlayerDiscovered(HeosPlayer *heosPlayer); + void onHeosPlayStateReceived(int playerId, Heos::HeosPlayerState state); + void onHeosShuffleModeReceived(int playerId, bool shuffle); + void onHeosRepeatModeReceived(int playerId, Heos::HeosRepeatMode repeatMode); + void onHeosMuteStatusReceived(int playerId, bool mute); + void onHeosVolumeStatusReceived(int playerId, int volume); + void onHeosNowPlayingMediaStatusReceived(int playerId, QString source, QString artist, QString album, QString Song, QString artwork); }; #endif // DEVICEPLUGINDENON_H diff --git a/denon/deviceplugindenon.json b/denon/deviceplugindenon.json index 1f9e28ca..6739d302 100644 --- a/denon/deviceplugindenon.json +++ b/denon/deviceplugindenon.json @@ -13,6 +13,8 @@ "name": "AVRX1000", "displayName": "AVR X1000", "createMethods": ["user"], + "deviceIcon": "Hifi", + "interfaces": ["connectable", "power", "extendedvolumecontroller"], "paramTypes": [ { "id": "a54b98b4-b78f-41dd-a257-14425c6cf9ab", @@ -42,16 +44,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 }, { @@ -85,6 +96,251 @@ ], "defaultValue": "TUNER" } + ], + "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": ["connectable", "extendedmediacontroller", "extendedvolumecontroller", "mediametadataprovider", "shufflerepeat" ], + "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..01bef7c6 --- /dev/null +++ b/denon/heos.cpp @@ -0,0 +1,475 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * * + * 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 + +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))); +} + + +void Heos::getPlayers() +{ + QByteArray cmd = "heos://player/get_players\r\n"; + m_socket->write(cmd); +} + +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::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, HeosPlayerState state) +{ + QByteArray playerStateQuery; + + if (state == HeosPlayerState::Play){ + playerStateQuery = "&state=play"; + } else if (state == HeosPlayerState::Pause){ + playerStateQuery = "&state=pause"; + } else if (state == HeosPlayerState::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, HeosRepeatMode repeatMode, bool shuffle) +{ + QByteArray repeatModeQuery; + + if (repeatMode == HeosRepeatMode::Off) { + repeatModeQuery = "&repeat=off"; + } else if (repeatMode == HeosRepeatMode::One) { + repeatModeQuery = "&repeat=on_one"; + } else if (repeatMode == HeosRepeatMode::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::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::getNowPlayingMedia(int playerId) +{ + QByteArray cmd = "heos://player/get_now_playing_media?pid=" + QVariant(playerId).toByteArray() + "\r\n"; + m_socket->write(cmd); +} + +Heos::~Heos() +{ + m_socket->close(); +} + +bool Heos::connected() +{ + return m_connected; +} + +void Heos::connectHeos() +{ + if (m_socket->state() == QAbstractSocket::ConnectingState) { + return; + } + m_socket->connectToHost(m_hostAddress, 1255); +} + +HeosPlayer *Heos::getPlayer(int playerId) +{ + return m_heosPlayers.value(playerId); +} + +void Heos::onConnected() +{ + qCDebug(dcDenon()) << "connected successfully to" << m_hostAddress.toString(); + setConnected(true); +} + +void Heos::onDisconnected() +{ + qCDebug(dcDenon()) << "disconnected from" << m_hostAddress.toString(); + setConnected(false); +} + +void Heos::onError(QAbstractSocket::SocketError socketError) +{ + qCWarning(dcDenon) << "socket error:" << socketError << m_socket->errorString(); +} + +void Heos::readData() +{ + int playerId; + 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(); + QString source; + switch (dataMap.value("payload").toMap().value("sid").toInt()) { + case 1: + source = "Pandora"; + break; + case 2: + source = "Rhapsody"; + break; + case 3: + source = "TuneIn"; + break; + case 4: + source = "Spotify"; + break; + case 5: + source = "Deezer"; + break; + case 6: + source = "Napster"; + break; + case 7: + source = "iHeartRadio"; + break; + case 8: + source = "Sirius XM"; + break; + case 9: + source = "Soundcloud"; + break; + case 10: + source = "Tidal"; + break; + case 11: + source = "Unknown"; + break; + case 12: + source = "Rdio"; + break; + case 13: + source = "Amazon Music"; + break; + case 14: + source = "Unknown"; + break; + case 15: + source = "Moodmix"; + break; + case 16: + source = "Juke"; + break; + case 17: + source = "Unkown"; + break; + case 18: + source = "QQMusic"; + break; + case 1024: + source = "USB Media/DLNA Servers"; + break; + case 1025: + source = "HEOS Playlists"; + break; + case 1026: + source = "HEOS History"; + break; + case 1027: + source = "HEOS aux inputs"; + break; + case 1028: + source = "HEOS aux inputs"; + break; + default: + source = "Unknown"; + }; + emit nowPlayingMediaStatusReceived(playerId, source, artist, album, song, artwork); + } + + if (command.contains("get_play_state") || command.contains("set_play_state")) { + if (message.hasQueryItem("state")) { + HeosPlayerState playState = HeosPlayerState::Stop; + if (message.queryItemValue("state").contains("play")) { + playState = HeosPlayerState::Play; + } else if (message.queryItemValue("state").contains("pause")) { + playState = HeosPlayerState::Pause; + } else if (message.queryItemValue("state").contains("stop")) { + playState = HeosPlayerState::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); + + HeosRepeatMode repeatMode = HeosRepeatMode::Off; + if (message.queryItemValue("repeat").contains("on_all")){ + repeatMode = HeosRepeatMode::All; + } else if (message.queryItemValue("repeat").contains("on_one")){ + repeatMode = HeosRepeatMode::One; + } else if (message.queryItemValue("repeat").contains("off")){ + repeatMode = HeosRepeatMode::Off; + } + emit repeatModeReceived(playerId, repeatMode); + } + } + + if (command.contains("player_state_changed")) { + if (message.hasQueryItem("state")) { + HeosPlayerState playState = HeosPlayerState::Stop; + if (message.queryItemValue("state").contains("play")) { + playState = HeosPlayerState::Play; + } else if (message.queryItemValue("state").contains("pause")) { + playState = HeosPlayerState::Pause; + } else if (message.queryItemValue("state").contains("stop")) { + playState = HeosPlayerState::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")) { + HeosRepeatMode repeatMode = HeosRepeatMode::Off; + if (message.queryItemValue("repeat").contains("on_all")){ + repeatMode = HeosRepeatMode::All; + } else if (message.queryItemValue("repeat").contains("on_one")){ + repeatMode = HeosRepeatMode::One; + } else if (message.queryItemValue("repeat").contains("off")){ + repeatMode = HeosRepeatMode::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); + } + } + } + } +} + +void Heos::setConnected(const bool &connected) +{ + m_connected = connected; + emit connectionStatusChanged(); +} diff --git a/denon/heos.h b/denon/heos.h new file mode 100644 index 00000000..6ef28651 --- /dev/null +++ b/denon/heos.h @@ -0,0 +1,76 @@ +#ifndef HEOS_H +#define HEOS_H + +#include +#include +#include +#include "heosplayer.h" + +class Heos : public QObject +{ + Q_OBJECT +public: + enum HeosPlayerState { + Play = 0, + Pause = 1, + Stop = 2 + }; + + enum HeosRepeatMode { + Off = 0, + One = 1, + All = 2 + }; + + explicit Heos(const QHostAddress &hostAddress, QObject *parent = 0); + ~Heos(); + + void setAddress(QHostAddress address); + QHostAddress getAddress(); + bool connected(); + void connectHeos(); + + void getPlayers(); + HeosPlayer *getPlayer(int playerId); + void getPlayerState(int playerId); + void setPlayerState(int playerId, HeosPlayerState state); + void getVolume(int playerId); + void setVolume(int playerId, int volume); + void getMute(int playerId); + void setMute(int playerId, bool state); + void setPlayMode(int playerId, HeosRepeatMode repeatMode, bool shuffle); //shuffle and repead mode + void getPlayMode(int playerId); + void playNext(int playerId); + void playPrevious(int playerId); + void getNowPlayingMedia(int playerId); + void registerForChangeEvents(bool state); + void sendHeartbeat(); + +private: + bool m_connected; + bool m_eventRegistered; + QHostAddress m_hostAddress; + QTcpSocket *m_socket; + QHash m_heosPlayers; + void setConnected(const bool &connected); + +signals: + void playerDiscovered(HeosPlayer *heosPlayer); + void connectionStatusChanged(); + + void playStateReceived(int playerId, HeosPlayerState state); + void shuffleModeReceived(int playerId, bool shuffle); + void repeatModeReceived(int playerId, HeosRepeatMode repeatMode); + void muteStatusReceived(int playerId, bool mute); + void volumeStatusReceived(int playerId, int volume); + void nowPlayingMediaStatusReceived(int playerId, QString 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..e6757d5c --- /dev/null +++ b/denon/heosplayer.cpp @@ -0,0 +1,74 @@ +#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..5fa4795e --- /dev/null +++ b/denon/heosplayer.h @@ -0,0 +1,42 @@ +#ifndef HEOSPLAYER_H +#define HEOSPLAYER_H + +#include + +class HeosPlayer : public QObject +{ + Q_OBJECT +public: + explicit HeosPlayer(int playerId, QObject *parent = 0); + explicit HeosPlayer(int playerId, QString name, QString serialNumber, QObject *parent = 0); + + 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; + + +signals: + +public slots: +}; + +#endif // HEOSPLAYER_H