From ac3a209afe6ec692557a8bc74b1998aa58bd62ed Mon Sep 17 00:00:00 2001 From: nymea Date: Fri, 10 May 2019 11:29:39 +0200 Subject: [PATCH 01/12] added denon heos --- denon/denon.pro | 8 +- denon/deviceplugindenon.cpp | 685 +++++++++++++++++++++++++++-------- denon/deviceplugindenon.h | 45 ++- denon/deviceplugindenon.json | 264 +++++++++++++- denon/heos.cpp | 475 ++++++++++++++++++++++++ denon/heos.h | 76 ++++ denon/heosplayer.cpp | 74 ++++ denon/heosplayer.h | 42 +++ 8 files changed, 1505 insertions(+), 164 deletions(-) create mode 100644 denon/heos.cpp create mode 100644 denon/heos.h create mode 100644 denon/heosplayer.cpp create mode 100644 denon/heosplayer.h 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 From 91da797dc54becea342e81578915d9dab7a1a43b Mon Sep 17 00:00:00 2001 From: nymea Date: Fri, 10 May 2019 13:00:13 +0200 Subject: [PATCH 02/12] added AVR-X1000 avahi discovery --- denon/deviceplugindenon.cpp | 40 +++++++++++++++++++++--------------- denon/deviceplugindenon.h | 1 - denon/deviceplugindenon.json | 3 +-- 3 files changed, 25 insertions(+), 19 deletions(-) diff --git a/denon/deviceplugindenon.cpp b/denon/deviceplugindenon.cpp index 91cfcf90..48907a22 100644 --- a/denon/deviceplugindenon.cpp +++ b/denon/deviceplugindenon.cpp @@ -60,8 +60,25 @@ DeviceManager::DeviceError DevicePluginDenon::discoverDevices(const DeviceClassI Q_UNUSED(params) if (deviceClassId == AVRX1000DeviceClassId) { - QtAvahiServiceBrowser *avahiBrowser = hardwareManager()->avahiBrowser(); - connect(avahiBrowser, &QtAvahiServiceBrowser::serviceEntryAdded, this, &DevicePluginDenon::onAvahiEntryAdded); + + QList deviceDescriptors; + foreach (const AvahiServiceEntry &service, hardwareManager()->avahiBrowser()->serviceEntries()) { + if (service.name().contains("AVR-X1000")) { + DeviceDescriptor deviceDescriptor(AVRX1000DeviceClassId, service.hostName().remove(".local"), service.hostAddress().toString()); + ParamList params; + qCDebug(dcDenon) << "Avahi discovered device: " << service.name() << service.hostName() << service.serviceType(); + params.append(Param(AVRX1000DeviceIpParamTypeId, service.hostAddress().toString())); + deviceDescriptor.setParams(params); + foreach (Device *existingDevice, myDevices()) { + if (existingDevice->paramValue(AVRX1000DeviceIpParamTypeId).toString() == service.hostAddress().toString()) { + deviceDescriptor.setDeviceId(existingDevice->id()); + break; + } + } + deviceDescriptors.append(deviceDescriptor); + } + } + emit devicesDiscovered(AVRX1000DeviceClassId, deviceDescriptors); return DeviceManager::DeviceErrorAsync; } @@ -173,7 +190,7 @@ Device::DeviceError DevicePluginDenon::executeAction(Device *device, const Actio qCDebug(dcDenon) << "set power action" << action.id(); qCDebug(dcDenon) << "power: " << action.param(AVRX1000PowerActionPowerParamTypeId).value().Bool; - if (action.param(AVRX1000PowerActionPowerParamTypeId).value().toBool() == true){ + if (action.param(AVRX1000PowerActionPowerParamTypeId).value().toBool() == true) { QByteArray cmd = "PWON\r"; qCDebug(dcDenon) << "Execute power: " << action.id() << cmd; denonConnection->sendData(cmd); @@ -192,8 +209,7 @@ Device::DeviceError DevicePluginDenon::executeAction(Device *device, const Actio qCDebug(dcDenon) << "Execute volume" << action.id() << cmd; denonConnection->sendData(cmd); - - return Device::DeviceErrorNoError; + return DeviceManager::DeviceErrorNoError; } else if (action.actionTypeId() == AVRX1000ChannelActionTypeId) { @@ -203,22 +219,20 @@ Device::DeviceError DevicePluginDenon::executeAction(Device *device, const Actio qCDebug(dcDenon) << "Change to channel:" << cmd; denonConnection->sendData(cmd); - 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 DeviceManager::DeviceErrorActionTypeNotFound; } @@ -297,7 +311,6 @@ Device::DeviceError DevicePluginDenon::executeAction(Device *device, const Actio heos->playNext(playerId); return DeviceManager::DeviceErrorNoError; } - return DeviceManager::DeviceErrorActionTypeNotFound; } return Device::DeviceErrorDeviceClassNotFound; @@ -358,7 +371,6 @@ void DevicePluginDenon::onPluginTimer() heos->registerForChangeEvents(true); } - if (device->deviceClassId() == heosPlayerDeviceClassId) { Device *heosDevice = myDevices().findById(device->parentId()); Heos *heos = m_heos.value(heosDevice); @@ -408,7 +420,7 @@ void DevicePluginDenon::onAVRDataReceived(const QByteArray &data) } if (data.contains("SI")) { - QString cmd = NULL; + QString cmd; if (data.contains("TUNER")) { cmd = "TUNER"; } else if (data.contains("DVD")) { @@ -528,10 +540,6 @@ void DevicePluginDenon::onUpnpDiscoveryFinished() emit devicesDiscovered(heosDeviceClassId, deviceDescriptors); } -void DevicePluginDenon::onAvahiEntryAdded() { - qCDebug(dcDenon) << "Avahi entry added"; -} - void DevicePluginDenon::onHeosConnectionChanged() { Heos *heos = static_cast(sender()); diff --git a/denon/deviceplugindenon.h b/denon/deviceplugindenon.h index a986ddf1..6db7b5ef 100644 --- a/denon/deviceplugindenon.h +++ b/denon/deviceplugindenon.h @@ -73,7 +73,6 @@ private slots: void onAVRSocketError(); void onUpnpDiscoveryFinished(); - void onAvahiEntryAdded(); void onHeosConnectionChanged(); void onHeosPlayerDiscovered(HeosPlayer *heosPlayer); diff --git a/denon/deviceplugindenon.json b/denon/deviceplugindenon.json index 6739d302..f6d6d0a8 100644 --- a/denon/deviceplugindenon.json +++ b/denon/deviceplugindenon.json @@ -12,8 +12,7 @@ "id": "1cd3d67e-aba0-450e-9e2a-483a1527aba6", "name": "AVRX1000", "displayName": "AVR X1000", - "createMethods": ["user"], - "deviceIcon": "Hifi", + "createMethods": ["discovery"], "interfaces": ["connectable", "power", "extendedvolumecontroller"], "paramTypes": [ { From 944a1262048be2914a52153f2e08da38639acbd7 Mon Sep 17 00:00:00 2001 From: nymea Date: Tue, 14 May 2019 08:55:03 +0200 Subject: [PATCH 03/12] fixed Denon Heos rediscovery --- denon/deviceplugindenon.cpp | 127 +++++++++++++----------------------- denon/deviceplugindenon.h | 13 ++-- denon/heos.h | 31 +++++++-- denon/heosplayer.cpp | 22 +++++++ denon/heosplayer.h | 30 +++++++-- 5 files changed, 125 insertions(+), 98 deletions(-) diff --git a/denon/deviceplugindenon.cpp b/denon/deviceplugindenon.cpp index 48907a22..27cf0fa5 100644 --- a/denon/deviceplugindenon.cpp +++ b/denon/deviceplugindenon.cpp @@ -131,7 +131,6 @@ Device::DeviceSetupStatus DevicePluginDenon::setupDevice(Device *device) 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); @@ -147,7 +146,6 @@ Device::DeviceSetupStatus DevicePluginDenon::setupDevice(Device *device) if (device->deviceClassId() == heosPlayerDeviceClassId) { return DeviceManager::DeviceSetupStatusSuccess; } - return DeviceManager::DeviceSetupStatusFailure; } @@ -171,9 +169,6 @@ void DevicePluginDenon::deviceRemoved(Device *device) } } - if (device->deviceClassId() == heosPlayerDeviceClassId) { - } - if (myDevices().empty()) { hardwareManager()->pluginTimerManager()->unregisterTimer(m_pluginTimer); } @@ -263,7 +258,6 @@ Device::DeviceError DevicePluginDenon::executeAction(Device *device, const Actio } else if (playbackStatus == "pausing") { heos->setPlayerState(playerId, Heos::HeosPlayerState::Pause); } - return DeviceManager::DeviceErrorNoError; } @@ -318,11 +312,8 @@ Device::DeviceError DevicePluginDenon::executeAction(Device *device, const Actio 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()); @@ -342,10 +333,6 @@ void DevicePluginDenon::postSetupDevice(Device *device) } } -void DevicePluginDenon::startMonitoringAutoDevices() -{ - -} void DevicePluginDenon::onPluginTimer() { @@ -462,7 +449,6 @@ void DevicePluginDenon::onAVRDataReceived(const QByteArray &data) } else if (data.contains("FVP")) { cmd = "FVP"; } - qCDebug(dcDenon) << "Update channel:" << cmd; device->setStateValue(AVRX1000ChannelStateTypeId, cmd); } @@ -497,7 +483,6 @@ void DevicePluginDenon::onAVRSocketError() } } - void DevicePluginDenon::onUpnpDiscoveryFinished() { qCDebug(dcDenon()) << "Upnp discovery finished"; @@ -511,32 +496,31 @@ void DevicePluginDenon::onUpnpDiscoveryFinished() 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"))) { + 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() == serialNumber) { + 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); deviceDescriptors.append(descriptor); - serialNumbers.append(serialNumber); } } } - emit devicesDiscovered(heosDeviceClassId, deviceDescriptors); } @@ -576,88 +560,69 @@ void DevicePluginDenon::onHeosPlayerDiscovered(HeosPlayer *heosPlayer) { 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"); - } - } + foreach(Device *device, myDevices().filterByParam(heosPlayerDevicePlayerIdParamTypeId, 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"); } + break; } } 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"); - } - } + foreach(Device *device, myDevices().filterByParam(heosPlayerDevicePlayerIdParamTypeId, 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"); } + break; } } 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); - } - } + foreach(Device *device, myDevices().filterByParam(heosPlayerDevicePlayerIdParamTypeId, playerId)) { + if (shuffle) { + device->setStateValue(heosPlayerMuteStateTypeId, true); + } else { + device->setStateValue(heosPlayerMuteStateTypeId, false); } + break; } } 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); - } - } + foreach(Device *device, myDevices().filterByParam(heosPlayerDevicePlayerIdParamTypeId, playerId)) { + device->setStateValue(heosPlayerMuteStateTypeId, mute); + break; } } 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); - } - } + foreach(Device *device, myDevices().filterByParam(heosPlayerDevicePlayerIdParamTypeId, playerId)) { + device->setStateValue(heosPlayerVolumeStateTypeId, volume); + break; } } 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); - } - } + foreach(Device *device, myDevices().filterByParam(heosPlayerDevicePlayerIdParamTypeId, playerId)) { + device->setStateValue(heosPlayerArtistStateTypeId, artist); + device->setStateValue(heosPlayerTitleStateTypeId, song); + device->setStateValue(heosPlayerArtworkStateTypeId, artwork); + device->setStateValue(heosPlayerCollectionStateTypeId, album); + device->setStateValue(heosPlayerSourceStateTypeId, source); + break; } } diff --git a/denon/deviceplugindenon.h b/denon/deviceplugindenon.h index 6db7b5ef..a71cad8e 100644 --- a/denon/deviceplugindenon.h +++ b/denon/deviceplugindenon.h @@ -24,19 +24,19 @@ #ifndef DEVICEPLUGINDENON_H #define DEVICEPLUGINDENON_H -#include "devices/deviceplugin.h" +#include "devicemanager.h" +#include "plugin/deviceplugin.h" +#include "plugintimer.h" +#include "denonconnection.h" +#include "heos.h" +#include #include #include #include #include #include -#include "plugintimer.h" -#include "denonconnection.h" -#include "heos.h" -#include - class DevicePluginDenon : public DevicePlugin { Q_OBJECT @@ -47,7 +47,6 @@ class DevicePluginDenon : public DevicePlugin public: explicit DevicePluginDenon(); - void startMonitoringAutoDevices() override; DeviceManager::DeviceError discoverDevices(const DeviceClassId &deviceClassId, const ParamList ¶ms) override; DeviceManager::DeviceSetupStatus setupDevice(Device *device) override; void postSetupDevice(Device * device) override; diff --git a/denon/heos.h b/denon/heos.h index 6ef28651..4cdc1755 100644 --- a/denon/heos.h +++ b/denon/heos.h @@ -1,9 +1,32 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * * + * 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" class Heos : public QObject @@ -22,7 +45,7 @@ public: All = 2 }; - explicit Heos(const QHostAddress &hostAddress, QObject *parent = 0); + explicit Heos(const QHostAddress &hostAddress, QObject *parent = nullptr); ~Heos(); void setAddress(QHostAddress address); @@ -47,10 +70,10 @@ public: void sendHeartbeat(); private: - bool m_connected; - bool m_eventRegistered; + bool m_connected = false; + bool m_eventRegistered = false; QHostAddress m_hostAddress; - QTcpSocket *m_socket; + QTcpSocket *m_socket = nullptr; QHash m_heosPlayers; void setConnected(const bool &connected); diff --git a/denon/heosplayer.cpp b/denon/heosplayer.cpp index e6757d5c..7ac95c89 100644 --- a/denon/heosplayer.cpp +++ b/denon/heosplayer.cpp @@ -1,3 +1,25 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * * + * 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) : diff --git a/denon/heosplayer.h b/denon/heosplayer.h index 5fa4795e..aa7a27ef 100644 --- a/denon/heosplayer.h +++ b/denon/heosplayer.h @@ -1,3 +1,25 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * * + * 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 @@ -7,8 +29,8 @@ 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); + explicit HeosPlayer(int playerId, QObject *parent = nullptr); + explicit HeosPlayer(int playerId, QString name, QString serialNumber, QObject *parent = nullptr); QString name(); void setName(QString name); @@ -33,10 +55,6 @@ private: QString m_playerVersion; QString m_control; - -signals: - -public slots: }; #endif // HEOSPLAYER_H From 89462334a29e3a4c6d174062031d407f011e28af Mon Sep 17 00:00:00 2001 From: nymea Date: Wed, 12 Jun 2019 21:00:57 +0200 Subject: [PATCH 04/12] switched from zeroconf to upnp discovery --- denon/deviceplugindenon.cpp | 41 +++++++++++++++++-------------------- denon/deviceplugindenon.h | 1 + 2 files changed, 20 insertions(+), 22 deletions(-) diff --git a/denon/deviceplugindenon.cpp b/denon/deviceplugindenon.cpp index 27cf0fa5..5a54ae3c 100644 --- a/denon/deviceplugindenon.cpp +++ b/denon/deviceplugindenon.cpp @@ -43,13 +43,17 @@ #include "deviceplugindenon.h" #include "plugininfo.h" +#include "plugin/device.h" +#include "network/networkaccessmanager.h" #include "network/upnp/upnpdiscovery.h" #include "network/upnp/upnpdiscoveryreply.h" -#include "network/avahi/qtavahiservicebrowser.h" +#include "platform/platformzeroconfcontroller.h" +#include "network/zeroconf/zeroconfservicebrowser.h" #include #include #include +#include DevicePluginDenon::DevicePluginDenon() { @@ -61,24 +65,8 @@ DeviceManager::DeviceError DevicePluginDenon::discoverDevices(const DeviceClassI if (deviceClassId == AVRX1000DeviceClassId) { - QList deviceDescriptors; - foreach (const AvahiServiceEntry &service, hardwareManager()->avahiBrowser()->serviceEntries()) { - if (service.name().contains("AVR-X1000")) { - DeviceDescriptor deviceDescriptor(AVRX1000DeviceClassId, service.hostName().remove(".local"), service.hostAddress().toString()); - ParamList params; - qCDebug(dcDenon) << "Avahi discovered device: " << service.name() << service.hostName() << service.serviceType(); - params.append(Param(AVRX1000DeviceIpParamTypeId, service.hostAddress().toString())); - deviceDescriptor.setParams(params); - foreach (Device *existingDevice, myDevices()) { - if (existingDevice->paramValue(AVRX1000DeviceIpParamTypeId).toString() == service.hostAddress().toString()) { - deviceDescriptor.setDeviceId(existingDevice->id()); - break; - } - } - deviceDescriptors.append(deviceDescriptor); - } - } - emit devicesDiscovered(AVRX1000DeviceClassId, deviceDescriptors); + UpnpDiscoveryReply *reply = hardwareManager()->upnpDiscovery()->discoverDevices("urn:schemas-upnp-org:device:MediaRenderer:1", "nymea", 7000); + connect(reply, &UpnpDiscoveryReply::finished, this, &DevicePluginDenon::onUpnpDiscoveryFinished); return DeviceManager::DeviceErrorAsync; } @@ -497,7 +485,8 @@ void DevicePluginDenon::onUpnpDiscoveryFinished() return; } - QList deviceDescriptors; + QList heosDescriptors; + QList avrDescriptors; foreach (const UpnpDeviceDescriptor &upnpDevice, reply->deviceDescriptors()) { if (upnpDevice.modelName().contains("HEOS")) { @@ -517,11 +506,19 @@ void DevicePluginDenon::onUpnpDiscoveryFinished() params.append(Param(heosDeviceIpParamTypeId, upnpDevice.hostAddress().toString())); params.append(Param(heosDeviceSerialNumberParamTypeId, serialNumber)); descriptor.setParams(params); - deviceDescriptors.append(descriptor); + heosDescriptors.append(descriptor); } } + //if (upnpDevice.modelName().contains("")) { + 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); + } + if (!avrDescriptors.isEmpty()) { + emit devicesDiscovered(AVRX1000DeviceClassId, avrDescriptors); } - emit devicesDiscovered(heosDeviceClassId, deviceDescriptors); } void DevicePluginDenon::onHeosConnectionChanged() diff --git a/denon/deviceplugindenon.h b/denon/deviceplugindenon.h index a71cad8e..7da3f97a 100644 --- a/denon/deviceplugindenon.h +++ b/denon/deviceplugindenon.h @@ -67,6 +67,7 @@ private: private slots: void onPluginTimer(); + void onAVRConnectionChanged(); void onAVRDataReceived(const QByteArray &data); void onAVRSocketError(); From 8d60bbb719c69cbfd329b986261fe62553da647f Mon Sep 17 00:00:00 2001 From: nymea Date: Fri, 28 Jun 2019 20:14:43 +0200 Subject: [PATCH 05/12] refactored AVR connection --- denon/avrconnection.cpp | 261 ++++++++++++++++++ denon/{denonconnection.h => avrconnection.h} | 53 ++-- denon/denon.pro | 4 +- denon/denonconnection.cpp | 108 -------- denon/deviceplugindenon.cpp | 271 +++++++++---------- denon/deviceplugindenon.h | 28 +- denon/deviceplugindenon.json | 40 ++- 7 files changed, 466 insertions(+), 299 deletions(-) create mode 100644 denon/avrconnection.cpp rename denon/{denonconnection.h => avrconnection.h} (63%) delete mode 100644 denon/denonconnection.cpp diff --git a/denon/avrconnection.cpp b/denon/avrconnection.cpp new file mode 100644 index 00000000..24820976 --- /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::connect() +{ + if (m_socket->state() == QAbstractSocket::ConnectingState) { + return; + } + m_socket->connectToHost(m_hostAddress, m_port); +} + +void AvrConnection::disconnect() +{ + 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..8bf2ab20 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 connect(); + void disconnect(); 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 41adb02f..34ff584a 100644 --- a/denon/denon.pro +++ b/denon/denon.pro @@ -6,12 +6,12 @@ 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 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 5a54ae3c..92e1e99e 100644 --- a/denon/deviceplugindenon.cpp +++ b/denon/deviceplugindenon.cpp @@ -43,7 +43,7 @@ #include "deviceplugindenon.h" #include "plugininfo.h" -#include "plugin/device.h" +#include "devices/device.h" #include "network/networkaccessmanager.h" #include "network/upnp/upnpdiscovery.h" #include "network/upnp/upnpdiscoveryreply.h" @@ -59,7 +59,7 @@ DevicePluginDenon::DevicePluginDenon() { } -DeviceManager::DeviceError DevicePluginDenon::discoverDevices(const DeviceClassId &deviceClassId, const ParamList ¶ms) +Device::DeviceError DevicePluginDenon::discoverDevices(const DeviceClassId &deviceClassId, const ParamList ¶ms) { Q_UNUSED(params) @@ -67,7 +67,7 @@ DeviceManager::DeviceError DevicePluginDenon::discoverDevices(const DeviceClassI UpnpDiscoveryReply *reply = hardwareManager()->upnpDiscovery()->discoverDevices("urn:schemas-upnp-org:device:MediaRenderer:1", "nymea", 7000); connect(reply, &UpnpDiscoveryReply::finished, this, &DevicePluginDenon::onUpnpDiscoveryFinished); - return DeviceManager::DeviceErrorAsync; + return Device::DeviceErrorAsync; } if (deviceClassId == heosDeviceClassId) { @@ -80,9 +80,9 @@ DeviceManager::DeviceError DevicePluginDenon::discoverDevices(const DeviceClassI */ UpnpDiscoveryReply *reply = hardwareManager()->upnpDiscovery()->discoverDevices(); connect(reply, &UpnpDiscoveryReply::finished, this, &DevicePluginDenon::onUpnpDiscoveryFinished); - return DeviceManager::DeviceErrorAsync; + return Device::DeviceErrorAsync; } - return DeviceManager::DeviceErrorDeviceClassNotFound; + return Device::DeviceErrorDeviceClassNotFound; } Device::DeviceSetupStatus DevicePluginDenon::setupDevice(Device *device) @@ -98,18 +98,22 @@ Device::DeviceSetupStatus DevicePluginDenon::setupDevice(Device *device) QHostAddress address(device->paramValue(AVRX1000DeviceIpParamTypeId).toString()); if (address.isNull()) { qCWarning(dcDenon) << "Could not parse ip address" << device->paramValue(AVRX1000DeviceIpParamTypeId).toString(); - return DeviceManager::DeviceSetupStatusFailure; + return Device::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); + 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_asyncSetups.append(denonConnection); - denonConnection->connectDenon(); - m_denonConnections.insert(device, denonConnection); - return DeviceManager::DeviceSetupStatusAsync; + denonConnection->connect(); + m_avrConnections.insert(device, denonConnection); + return Device::DeviceSetupStatusAsync; } if (device->deviceClassId() == heosDeviceClassId) { @@ -128,13 +132,13 @@ Device::DeviceSetupStatus DevicePluginDenon::setupDevice(Device *device) heos->connectHeos(); m_heos.insert(device, heos); - return DeviceManager::DeviceSetupStatusAsync; + return Device::DeviceSetupStatusAsync; } if (device->deviceClassId() == heosPlayerDeviceClassId) { - return DeviceManager::DeviceSetupStatusSuccess; + return Device::DeviceSetupStatusSuccess; } - return DeviceManager::DeviceSetupStatusFailure; + return Device::DeviceSetupStatusFailure; } void DevicePluginDenon::deviceRemoved(Device *device) @@ -142,17 +146,17 @@ void DevicePluginDenon::deviceRemoved(Device *device) qCDebug(dcDenon) << "Delete " << device->name(); if (device->deviceClassId() == AVRX1000DeviceClassId) { - DenonConnection *denonConnection = m_denonConnections.value(device); - m_denonConnections.remove(device); - denonConnection->disconnectDenon(); + AvrConnection *denonConnection = m_avrConnections.value(device); + m_avrConnections.remove(device); + denonConnection->disconnect(); denonConnection->deleteLater(); } if (device->deviceClassId() == heosDeviceClassId) { - if (m_denonConnections.contains(device)) { - DenonConnection *denonConnection = m_denonConnections.value(device); - m_denonConnections.remove(device); - denonConnection->disconnectDenon(); + if (m_avrConnections.contains(device)) { + AvrConnection *denonConnection = m_avrConnections.value(device); + m_avrConnections.remove(device); + denonConnection->disconnect(); denonConnection->deleteLater(); } } @@ -166,57 +170,44 @@ Device::DeviceError DevicePluginDenon::executeAction(Device *device, const Actio { qCDebug(dcDenon) << "Execute action" << device->id() << action.id() << action.params(); if (device->deviceClassId() == AVRX1000DeviceClassId) { - DenonConnection *denonConnection = m_denonConnections.value(device); + AvrConnection *avrConnection = m_avrConnections.value(device); if (action.actionTypeId() == AVRX1000PowerActionTypeId) { - 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; - denonConnection->sendData(cmd); - } else { - QByteArray cmd = "PWSTANDBY\r"; - qCDebug(dcDenon) << "Execute power: " << action.id() << cmd; - 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; - denonConnection->sendData(cmd); - return DeviceManager::DeviceErrorNoError; + 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"; - - qCDebug(dcDenon) << "Change to channel:" << cmd; - denonConnection->sendData(cmd); - return DeviceManager::DeviceErrorNoError; + avrConnection->setChannel(channel); + return Device::DeviceErrorNoError; } else if (action.actionTypeId() == AVRX1000IncreaseVolumeActionTypeId) { - QByteArray cmd = "MVUP\r"; - qCDebug(dcDenon) << "Execute volume increase" << action.id() << cmd; - denonConnection->sendData(cmd); - return DeviceManager::DeviceErrorNoError; + + avrConnection->increaseVolume(); + return Device::DeviceErrorNoError; } else if (action.actionTypeId() == AVRX1000DecreaseVolumeActionTypeId) { - QByteArray cmd = "MVDOWN\r"; - qCDebug(dcDenon) << "Execute volume decrease" << action.id() << cmd; - denonConnection->sendData(cmd); - return DeviceManager::DeviceErrorNoError; + + avrConnection->decreaseVolume(); + return Device::DeviceErrorNoError; + + } else if (action.actionTypeId() == AVRX1000SurroundModeActionTypeId) { + + QByteArray surroundMode = action.param(AVRX1000SurroundModeActionSurroundModeParamTypeId).value().toByteArray(); + avrConnection->setSurroundMode(surroundMode); + return Device::DeviceErrorNoError; } - return DeviceManager::DeviceErrorActionTypeNotFound; + return Device::DeviceErrorActionTypeNotFound; } if (device->deviceClassId() == heosPlayerDeviceClassId) { @@ -228,13 +219,13 @@ Device::DeviceError DevicePluginDenon::executeAction(Device *device, const Actio if (action.actionTypeId() == heosPlayerVolumeActionTypeId) { int volume = action.param(heosPlayerVolumeActionVolumeParamTypeId).value().toInt(); heos->setVolume(playerId, volume); - return DeviceManager::DeviceErrorNoError; + return Device::DeviceErrorNoError; } if (action.actionTypeId() == heosPlayerMuteActionTypeId) { bool mute = action.param(heosPlayerMuteActionMuteParamTypeId).value().toBool(); heos->setMute(playerId, mute); - return DeviceManager::DeviceErrorNoError; + return Device::DeviceErrorNoError; } if (action.actionTypeId() == heosPlayerPlaybackStatusActionTypeId) { @@ -246,7 +237,7 @@ Device::DeviceError DevicePluginDenon::executeAction(Device *device, const Actio } else if (playbackStatus == "pausing") { heos->setPlayerState(playerId, Heos::HeosPlayerState::Pause); } - return DeviceManager::DeviceErrorNoError; + return Device::DeviceErrorNoError; } if (action.actionTypeId() == heosPlayerShuffleActionTypeId) { @@ -255,45 +246,45 @@ Device::DeviceError DevicePluginDenon::executeAction(Device *device, const Actio repeatMode = Heos::HeosRepeatMode::Off; heos->setPlayMode(playerId, repeatMode, shuffle); - return DeviceManager::DeviceErrorNoError; + return Device::DeviceErrorNoError; } if (action.actionTypeId() == heosPlayerSkipBackActionTypeId) { heos->playPrevious(playerId); - return DeviceManager::DeviceErrorNoError; + return Device::DeviceErrorNoError; } if (action.actionTypeId() == heosPlayerFastRewindActionTypeId) { - return DeviceManager::DeviceErrorActionTypeNotFound; + return Device::DeviceErrorActionTypeNotFound; } if (action.actionTypeId() == heosPlayerStopActionTypeId) { heos->setPlayerState(playerId, Heos::HeosPlayerState::Stop); - return DeviceManager::DeviceErrorNoError; + return Device::DeviceErrorNoError; } if (action.actionTypeId() == heosPlayerPlayActionTypeId) { heos->setPlayerState(playerId, Heos::HeosPlayerState::Play); - return DeviceManager::DeviceErrorNoError; + return Device::DeviceErrorNoError; } if (action.actionTypeId() == heosPlayerPauseActionTypeId) { heos->setPlayerState(playerId, Heos::HeosPlayerState::Pause); - return DeviceManager::DeviceErrorNoError; + return Device::DeviceErrorNoError; } if (action.actionTypeId() == heosPlayerFastForwardActionTypeId) { - return DeviceManager::DeviceErrorActionTypeNotFound; + return Device::DeviceErrorActionTypeNotFound; } if (action.actionTypeId() == heosPlayerSkipNextActionTypeId) { heos->playNext(playerId); - return DeviceManager::DeviceErrorNoError; + return Device::DeviceErrorNoError; } - return DeviceManager::DeviceErrorActionTypeNotFound; + return Device::DeviceErrorActionTypeNotFound; } return Device::DeviceErrorDeviceClassNotFound; } @@ -324,13 +315,13 @@ void DevicePluginDenon::postSetupDevice(Device *device) void DevicePluginDenon::onPluginTimer() { - foreach(DenonConnection *denonConnection, m_denonConnections.values()) { + foreach(AvrConnection *denonConnection, m_avrConnections.values()) { if (!denonConnection->connected()) { - denonConnection->connectDenon(); + denonConnection->connect(); } - Device *device = m_denonConnections.key(denonConnection); + Device *device = m_avrConnections.key(denonConnection); if (device->deviceClassId() == AVRX1000DeviceClassId) { - denonConnection->sendData("PW?\rSI?\rMV?\r"); + denonConnection->getAllStatus(); } } @@ -360,113 +351,94 @@ void DevicePluginDenon::onPluginTimer() } } -void DevicePluginDenon::onAVRConnectionChanged() +void DevicePluginDenon::onAvrConnectionChanged(bool status) { - DenonConnection *denonConnection = static_cast(sender()); - Device *device = m_denonConnections.key(denonConnection); + AvrConnection *denonConnection = static_cast(sender()); + Device *device = m_avrConnections.key(denonConnection); + if (!device) + return; if (device->deviceClassId() == AVRX1000DeviceClassId) { // if the device is connected - if (denonConnection->connected()) { + if (status) { // 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); + + emit deviceSetupFinished(device, Device::DeviceSetupStatusSuccess); } } device->setStateValue(AVRX1000ConnectedStateTypeId, denonConnection->connected()); } } -void DevicePluginDenon::onAVRDataReceived(const QByteArray &data) +void DevicePluginDenon::onAvrVolumeChanged(int volume) { - DenonConnection *denonConnection = static_cast(sender()); - Device *device = m_denonConnections.key(denonConnection); - qCDebug(dcDenon) << "Data received" << data; + AvrConnection *denonConnection = static_cast(sender()); + Device *device = m_avrConnections.key(denonConnection); + if (!device) + 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(); + device->setStateValue(AVRX1000VolumeStateTypeId, volume); + } +} - qCDebug(dcDenon) << "Update volume:" << vol; - device->setStateValue(AVRX1000VolumeStateTypeId, vol); - } +void DevicePluginDenon::onAvrMuteChanged(bool mute) +{ + AvrConnection *denonConnection = static_cast(sender()); + Device *device = m_avrConnections.key(denonConnection); + if (!device) + return; - 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 channel:" << cmd; - device->setStateValue(AVRX1000ChannelStateTypeId, cmd); - } + if (device->deviceClassId() == AVRX1000DeviceClassId) { + device->setStateValue(AVRX1000MuteStateTypeId, mute); + } +} - 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::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() +void DevicePluginDenon::onAvrSocketError() { - DenonConnection *denonConnection = static_cast(sender()); - Device *device = m_denonConnections.key(denonConnection); + 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_asyncSetups.contains(denonConnection)) { m_asyncSetups.removeAll(denonConnection); qCWarning(dcDenon()) << "Could not add device. The setup failed."; - emit deviceSetupFinished(device, DeviceManager::DeviceSetupStatusFailure); + 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_denonConnections.remove(device); + m_avrConnections.remove(device); } } } @@ -510,7 +482,7 @@ void DevicePluginDenon::onUpnpDiscoveryFinished() } } //if (upnpDevice.modelName().contains("")) { - qCDebug(dcDenon) << "UPnP device found:" << upnpDevice.modelDescription() << upnpDevice.friendlyName() << upnpDevice.hostAddress().toString() << upnpDevice.modelName() << upnpDevice.manufacturer() << upnpDevice.serialNumber(); + qCDebug(dcDenon) << "UPnP device found:" << upnpDevice.modelDescription() << upnpDevice.friendlyName() << upnpDevice.hostAddress().toString() << upnpDevice.modelName() << upnpDevice.manufacturer() << upnpDevice.serialNumber(); //} } if (!heosDescriptors.isEmpty()) { @@ -527,7 +499,7 @@ void DevicePluginDenon::onHeosConnectionChanged() heos->registerForChangeEvents(true); Device *device = m_heos.key(heos); if (!device->setupComplete() && heos->connected()) { - emit deviceSetupFinished(device, DeviceManager::DeviceSetupStatusSuccess); + emit deviceSetupFinished(device, Device::DeviceSetupStatusSuccess); } } @@ -623,3 +595,4 @@ void DevicePluginDenon::onHeosNowPlayingMediaStatusReceived(int playerId, QStrin break; } } + diff --git a/denon/deviceplugindenon.h b/denon/deviceplugindenon.h index 7da3f97a..42421a14 100644 --- a/denon/deviceplugindenon.h +++ b/denon/deviceplugindenon.h @@ -24,10 +24,9 @@ #ifndef DEVICEPLUGINDENON_H #define DEVICEPLUGINDENON_H -#include "devicemanager.h" -#include "plugin/deviceplugin.h" +#include "devices/deviceplugin.h" #include "plugintimer.h" -#include "denonconnection.h" +#include "avrconnection.h" #include "heos.h" #include @@ -47,18 +46,18 @@ class DevicePluginDenon : public DevicePlugin public: explicit DevicePluginDenon(); - DeviceManager::DeviceError discoverDevices(const DeviceClassId &deviceClassId, const ParamList ¶ms) override; - DeviceManager::DeviceSetupStatus setupDevice(Device *device) override; + Device::DeviceError discoverDevices(const DeviceClassId &deviceClassId, const ParamList ¶ms) override; + Device::DeviceSetupStatus setupDevice(Device *device) override; void postSetupDevice(Device * device) override; - DeviceManager::DeviceError executeAction(Device *device, const Action &action) override; + Device::DeviceError executeAction(Device *device, const Action &action) override; void deviceRemoved(Device *device) override; private: PluginTimer *m_pluginTimer = nullptr; - QHash m_denonConnections; + QHash m_avrConnections; QHash m_heos; - QList m_asyncSetups; + QList m_asyncSetups; QHash m_playerIds; QHash m_discoveredPlayerIds; @@ -67,11 +66,6 @@ private: private slots: void onPluginTimer(); - - void onAVRConnectionChanged(); - void onAVRDataReceived(const QByteArray &data); - void onAVRSocketError(); - void onUpnpDiscoveryFinished(); void onHeosConnectionChanged(); @@ -82,6 +76,14 @@ private slots: 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); + + 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 f6d6d0a8..562fe538 100644 --- a/denon/deviceplugindenon.json +++ b/denon/deviceplugindenon.json @@ -94,6 +94,32 @@ "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": [ @@ -156,8 +182,8 @@ { "id": "4d1790bf-28c6-4c1f-8892-ba1a0ef140f5", "name": "connected", - "displayName": "connected", - "displayNameEvent": "connected changed", + "displayName": "Connected", + "displayNameEvent": "Connected changed", "defaultValue": false, "type": "bool" } @@ -200,7 +226,7 @@ "id": "9a4e527e-057c-4b19-8a02-605cc8349f5e", "name": "connected", "displayName": "connected", - "displayNameEvent": "connected changed", + "displayNameEvent": "Connected changed", "type": "bool", "defaultValue": false, "cached": false @@ -208,9 +234,9 @@ { "id": "fcc89c7c-b793-4b6f-a3dc-0e0e3a86748f", "name": "mute", - "displayName": "mute", - "displayNameEvent": "mute changed", - "displayNameAction": "set mute", + "displayName": "Mute", + "displayNameEvent": "Mute changed", + "displayNameAction": "Set mute", "type": "bool", "defaultValue": false, "cached": false, @@ -219,7 +245,7 @@ { "id": "6d4886a1-fa5d-4889-96c5-7a1c206f59be", "name": "volume", - "displayName": "volume", + "displayName": "Volume", "displayNameEvent": "volume changed", "displayNameAction": "set volume", "type": "int", From ab629725405df94f56d42d19d9ce9fa8abb9a676 Mon Sep 17 00:00:00 2001 From: nymea Date: Fri, 28 Jun 2019 20:26:18 +0200 Subject: [PATCH 06/12] fixed connect overloading --- denon/avrconnection.cpp | 4 ++-- denon/avrconnection.h | 4 ++-- denon/deviceplugindenon.cpp | 8 ++++---- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/denon/avrconnection.cpp b/denon/avrconnection.cpp index 24820976..91440d48 100644 --- a/denon/avrconnection.cpp +++ b/denon/avrconnection.cpp @@ -43,7 +43,7 @@ AvrConnection::~AvrConnection() m_socket->close(); } -void AvrConnection::connect() +void AvrConnection::connectDevice() { if (m_socket->state() == QAbstractSocket::ConnectingState) { return; @@ -51,7 +51,7 @@ void AvrConnection::connect() m_socket->connectToHost(m_hostAddress, m_port); } -void AvrConnection::disconnect() +void AvrConnection::disconnectDevice() { m_socket->close(); } diff --git a/denon/avrconnection.h b/denon/avrconnection.h index 8bf2ab20..c64297f8 100644 --- a/denon/avrconnection.h +++ b/denon/avrconnection.h @@ -35,8 +35,8 @@ public: explicit AvrConnection(const QHostAddress &hostAddress, const int &port = 23, QObject *parent = nullptr); ~AvrConnection(); - void connect(); - void disconnect(); + void connectDevice(); + void disconnectDevice(); QHostAddress hostAddress() const; int port() const; diff --git a/denon/deviceplugindenon.cpp b/denon/deviceplugindenon.cpp index 92e1e99e..e65a4334 100644 --- a/denon/deviceplugindenon.cpp +++ b/denon/deviceplugindenon.cpp @@ -111,7 +111,7 @@ Device::DeviceSetupStatus DevicePluginDenon::setupDevice(Device *device) connect(denonConnection, &AvrConnection::muteChanged, this, &DevicePluginDenon::onAvrMuteChanged); m_asyncSetups.append(denonConnection); - denonConnection->connect(); + denonConnection->connectDevice(); m_avrConnections.insert(device, denonConnection); return Device::DeviceSetupStatusAsync; } @@ -148,7 +148,7 @@ void DevicePluginDenon::deviceRemoved(Device *device) if (device->deviceClassId() == AVRX1000DeviceClassId) { AvrConnection *denonConnection = m_avrConnections.value(device); m_avrConnections.remove(device); - denonConnection->disconnect(); + denonConnection->disconnectDevice(); denonConnection->deleteLater(); } @@ -156,7 +156,7 @@ void DevicePluginDenon::deviceRemoved(Device *device) if (m_avrConnections.contains(device)) { AvrConnection *denonConnection = m_avrConnections.value(device); m_avrConnections.remove(device); - denonConnection->disconnect(); + denonConnection->disconnectDevice(); denonConnection->deleteLater(); } } @@ -317,7 +317,7 @@ void DevicePluginDenon::onPluginTimer() { foreach(AvrConnection *denonConnection, m_avrConnections.values()) { if (!denonConnection->connected()) { - denonConnection->connect(); + denonConnection->connectDevice(); } Device *device = m_avrConnections.key(denonConnection); if (device->deviceClassId() == AVRX1000DeviceClassId) { From 6b1b22fa4650a9642b95f63830d8d45abec413d6 Mon Sep 17 00:00:00 2001 From: nymea Date: Fri, 28 Jun 2019 20:34:48 +0200 Subject: [PATCH 07/12] added missing symbol --- denon/deviceplugindenon.cpp | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/denon/deviceplugindenon.cpp b/denon/deviceplugindenon.cpp index e65a4334..c4063ae0 100644 --- a/denon/deviceplugindenon.cpp +++ b/denon/deviceplugindenon.cpp @@ -384,6 +384,18 @@ void DevicePluginDenon::onAvrVolumeChanged(int 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()); From 4db6b6bddba4b717a85272bce34e9dd3f5306c2f Mon Sep 17 00:00:00 2001 From: nymea Date: Mon, 8 Jul 2019 20:07:40 +0200 Subject: [PATCH 08/12] fixed avr discovery --- denon/deviceplugindenon.cpp | 48 ++++++++++++++++++++++++++++++++---- denon/deviceplugindenon.h | 13 +++++++--- denon/deviceplugindenon.json | 12 ++++++--- 3 files changed, 62 insertions(+), 11 deletions(-) diff --git a/denon/deviceplugindenon.cpp b/denon/deviceplugindenon.cpp index c4063ae0..734d361c 100644 --- a/denon/deviceplugindenon.cpp +++ b/denon/deviceplugindenon.cpp @@ -64,9 +64,41 @@ Device::DeviceError DevicePluginDenon::discoverDevices(const DeviceClassId &devi Q_UNUSED(params) 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; - UpnpDiscoveryReply *reply = hardwareManager()->upnpDiscovery()->discoverDevices("urn:schemas-upnp-org:device:MediaRenderer:1", "nymea", 7000); - connect(reply, &UpnpDiscoveryReply::finished, this, &DevicePluginDenon::onUpnpDiscoveryFinished); + 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; } @@ -500,9 +532,6 @@ void DevicePluginDenon::onUpnpDiscoveryFinished() if (!heosDescriptors.isEmpty()) { emit devicesDiscovered(heosDeviceClassId, heosDescriptors); } - if (!avrDescriptors.isEmpty()) { - emit devicesDiscovered(AVRX1000DeviceClassId, avrDescriptors); - } } void DevicePluginDenon::onHeosConnectionChanged() @@ -608,3 +637,12 @@ void DevicePluginDenon::onHeosNowPlayingMediaStatusReceived(int playerId, QStrin } } +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 42421a14..fbbeb121 100644 --- a/denon/deviceplugindenon.h +++ b/denon/deviceplugindenon.h @@ -24,11 +24,14 @@ #ifndef DEVICEPLUGINDENON_H #define DEVICEPLUGINDENON_H -#include "devices/deviceplugin.h" -#include "plugintimer.h" -#include "avrconnection.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 @@ -54,6 +57,8 @@ public: private: PluginTimer *m_pluginTimer = nullptr; + ZeroConfServiceBrowser *m_serviceBrowser = nullptr; + QHash m_avrConnections; QHash m_heos; @@ -77,6 +82,8 @@ private slots: void onHeosVolumeStatusReceived(int playerId, int volume); void onHeosNowPlayingMediaStatusReceived(int playerId, QString 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); diff --git a/denon/deviceplugindenon.json b/denon/deviceplugindenon.json index 562fe538..cce1b92b 100644 --- a/denon/deviceplugindenon.json +++ b/denon/deviceplugindenon.json @@ -13,14 +13,20 @@ "name": "AVRX1000", "displayName": "AVR X1000", "createMethods": ["discovery"], - "interfaces": ["connectable", "power", "extendedvolumecontroller"], + "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": [ @@ -194,7 +200,7 @@ "name": "heosPlayer", "displayName": "Heos player", "createMethods": ["auto"], - "interfaces": ["connectable", "extendedmediacontroller", "extendedvolumecontroller", "mediametadataprovider", "shufflerepeat" ], + "interfaces": ["extendedmediacontroller", "extendedvolumecontroller", "mediametadataprovider", "shufflerepeat", "connectable"], "paramTypes":[ { "id": "89629008-6ad8-4e92-863d-b86e0e012d0b", From 0eb0933b34777dde38168e2acf4defae13371821 Mon Sep 17 00:00:00 2001 From: nymea Date: Fri, 12 Jul 2019 18:14:19 +0200 Subject: [PATCH 09/12] fixed heos rediscovery --- denon/denon.pro | 3 +- denon/deviceplugindenon.cpp | 169 ++++++++++++----- denon/deviceplugindenon.h | 11 +- denon/heos.cpp | 363 +++++++++++++++++++++++------------- denon/heos.h | 79 +++++--- denon/heosplayer.cpp | 1 - denon/heostypes.h | 137 ++++++++++++++ 7 files changed, 560 insertions(+), 203 deletions(-) create mode 100644 denon/heostypes.h diff --git a/denon/denon.pro b/denon/denon.pro index 34ff584a..bc6a7b8d 100644 --- a/denon/denon.pro +++ b/denon/denon.pro @@ -14,4 +14,5 @@ HEADERS += \ deviceplugindenon.h \ heos.h \ heosplayer.h \ - avrconnection.h + avrconnection.h \ + heostypes.h diff --git a/denon/deviceplugindenon.cpp b/denon/deviceplugindenon.cpp index 734d361c..be3b9c7c 100644 --- a/denon/deviceplugindenon.cpp +++ b/denon/deviceplugindenon.cpp @@ -81,7 +81,6 @@ Device::DeviceError DevicePluginDenon::discoverDevices(const DeviceClassId &devi qCDebug(dcDenon) << "service discovered" << name << "ID:" << id; if (discoveredIds.contains(id)) break; - discoveredIds.append(id); DeviceDescriptor deviceDescriptor(AVRX1000DeviceClassId, name, address); ParamList params; @@ -142,7 +141,7 @@ Device::DeviceSetupStatus DevicePluginDenon::setupDevice(Device *device) connect(denonConnection, &AvrConnection::surroundModeChanged, this, &DevicePluginDenon::onAvrSurroundModeChanged); connect(denonConnection, &AvrConnection::muteChanged, this, &DevicePluginDenon::onAvrMuteChanged); - m_asyncSetups.append(denonConnection); + m_asyncAvrSetups.append(denonConnection); denonConnection->connectDevice(); m_avrConnections.insert(device, denonConnection); return Device::DeviceSetupStatusAsync; @@ -162,6 +161,7 @@ Device::DeviceSetupStatus DevicePluginDenon::setupDevice(Device *device) connect(heos, &Heos::volumeStatusReceived, this, &DevicePluginDenon::onHeosVolumeStatusReceived); connect(heos, &Heos::nowPlayingMediaStatusReceived, this, &DevicePluginDenon::onHeosNowPlayingMediaStatusReceived); + m_asyncHeosSetups.append(heos); heos->connectHeos(); m_heos.insert(device, heos); return Device::DeviceSetupStatusAsync; @@ -263,21 +263,24 @@ Device::DeviceError DevicePluginDenon::executeAction(Device *device, const Actio if (action.actionTypeId() == heosPlayerPlaybackStatusActionTypeId) { QString playbackStatus = action.param(heosPlayerPlaybackStatusActionPlaybackStatusParamTypeId).value().toString(); if (playbackStatus == "playing") { - heos->setPlayerState(playerId, Heos::HeosPlayerState::Play); + heos->setPlayerState(playerId, PLAYER_STATE_PLAY); } else if (playbackStatus == "stopping") { - heos->setPlayerState(playerId, Heos::HeosPlayerState::Stop); + heos->setPlayerState(playerId, PLAYER_STATE_STOP); } else if (playbackStatus == "pausing") { - heos->setPlayerState(playerId, Heos::HeosPlayerState::Pause); + heos->setPlayerState(playerId, PLAYER_STATE_PAUSE); } return Device::DeviceErrorNoError; } if (action.actionTypeId() == heosPlayerShuffleActionTypeId) { bool shuffle = action.param(heosPlayerShuffleActionShuffleParamTypeId).value().toBool(); - Heos::HeosRepeatMode repeatMode; - repeatMode = Heos::HeosRepeatMode::Off; + 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; } @@ -292,18 +295,17 @@ Device::DeviceError DevicePluginDenon::executeAction(Device *device, const Actio } if (action.actionTypeId() == heosPlayerStopActionTypeId) { - heos->setPlayerState(playerId, Heos::HeosPlayerState::Stop); + heos->setPlayerState(playerId, PLAYER_STATE_STOP); return Device::DeviceErrorNoError; } if (action.actionTypeId() == heosPlayerPlayActionTypeId) { - heos->setPlayerState(playerId, Heos::HeosPlayerState::Play); + heos->setPlayerState(playerId, PLAYER_STATE_PLAY); return Device::DeviceErrorNoError; } if (action.actionTypeId() == heosPlayerPauseActionTypeId) { - - heos->setPlayerState(playerId, Heos::HeosPlayerState::Pause); + heos->setPlayerState(playerId, PLAYER_STATE_PAUSE); return Device::DeviceErrorNoError; } @@ -327,11 +329,10 @@ void DevicePluginDenon::postSetupDevice(Device *device) Heos *heos = m_heos.value(device); heos->getPlayers(); - device->setStateValue(heosConnectedStateTypeId, heos->connected()); } 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(); @@ -340,7 +341,6 @@ void DevicePluginDenon::postSetupDevice(Device *device) heos->getVolume(playerId); heos->getMute(playerId); heos->getNowPlayingMedia(playerId); - device->setStateValue(heosPlayerConnectedStateTypeId, heos->connected()); } } @@ -361,10 +361,6 @@ void DevicePluginDenon::onPluginTimer() 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); } @@ -394,8 +390,8 @@ void DevicePluginDenon::onAvrConnectionChanged(bool status) // if the device is connected if (status) { // and from the first setup - if (m_asyncSetups.contains(denonConnection)) { - m_asyncSetups.removeAll(denonConnection); + if (m_asyncAvrSetups.contains(denonConnection)) { + m_asyncAvrSetups.removeAll(denonConnection); emit deviceSetupFinished(device, Device::DeviceSetupStatusSuccess); } @@ -475,8 +471,8 @@ void DevicePluginDenon::onAvrSocketError() if (device->deviceClassId() == AVRX1000DeviceClassId) { // Check if setup running for this device - if (m_asyncSetups.contains(denonConnection)) { - m_asyncSetups.removeAll(denonConnection); + 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 @@ -502,7 +498,6 @@ void DevicePluginDenon::onUpnpDiscoveryFinished() } QList heosDescriptors; - QList avrDescriptors; foreach (const UpnpDeviceDescriptor &upnpDevice, reply->deviceDescriptors()) { if (upnpDevice.modelName().contains("HEOS")) { @@ -513,7 +508,7 @@ void DevicePluginDenon::onUpnpDiscoveryFinished() DeviceDescriptor descriptor(heosDeviceClassId, upnpDevice.modelName(), serialNumber); ParamList params; foreach (Device *existingDevice, myDevices()) { - if (existingDevice->paramValue(heosDeviceSerialNumberParamTypeId).toString() == serialNumber) { + if (existingDevice->paramValue(heosDeviceSerialNumberParamTypeId).toString().contains(serialNumber, Qt::CaseSensitivity::CaseInsensitive)) { descriptor.setDeviceId(existingDevice->id()); break; } @@ -525,22 +520,40 @@ void DevicePluginDenon::onUpnpDiscoveryFinished() heosDescriptors.append(descriptor); } } - //if (upnpDevice.modelName().contains("")) { 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() +void DevicePluginDenon::onHeosConnectionChanged(bool status) { Heos *heos = static_cast(sender()); heos->registerForChangeEvents(true); Device *device = m_heos.key(heos); - if (!device->setupComplete() && heos->connected()) { - emit deviceSetupFinished(device, Device::DeviceSetupStatusSuccess); + 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); + } + } + } } } @@ -568,14 +581,14 @@ void DevicePluginDenon::onHeosPlayerDiscovered(HeosPlayer *heosPlayer) { autoDevicesAppeared(heosPlayerDeviceClassId, heosPlayerDescriptors); } -void DevicePluginDenon::onHeosPlayStateReceived(int playerId, Heos::HeosPlayerState state) +void DevicePluginDenon::onHeosPlayStateReceived(int playerId, PLAYER_STATE state) { foreach(Device *device, myDevices().filterByParam(heosPlayerDevicePlayerIdParamTypeId, playerId)) { - if (state == Heos::HeosPlayerState::Pause) { + if (state == PLAYER_STATE_PAUSE) { device->setStateValue(heosPlayerPlaybackStatusStateTypeId, "Paused"); - } else if (state == Heos::HeosPlayerState::Play) { + } else if (state == PLAYER_STATE_PLAY) { device->setStateValue(heosPlayerPlaybackStatusStateTypeId, "Playing"); - } else if (state == Heos::HeosPlayerState::Stop) { + } else if (state == PLAYER_STATE_STOP) { device->setStateValue(heosPlayerPlaybackStatusStateTypeId, "Stopped"); } break; @@ -583,14 +596,14 @@ void DevicePluginDenon::onHeosPlayStateReceived(int playerId, Heos::HeosPlayerSt } -void DevicePluginDenon::onHeosRepeatModeReceived(int playerId, Heos::HeosRepeatMode repeatMode) +void DevicePluginDenon::onHeosRepeatModeReceived(int playerId, REPEAT_MODE repeatMode) { foreach(Device *device, myDevices().filterByParam(heosPlayerDevicePlayerIdParamTypeId, playerId)) { - if (repeatMode == Heos::HeosRepeatMode::All) { + if (repeatMode == REPEAT_MODE_ALL) { device->setStateValue(heosPlayerRepeatStateTypeId, "All"); - } else if (repeatMode == Heos::HeosRepeatMode::One) { + } else if (repeatMode == REPEAT_MODE_ONE) { device->setStateValue(heosPlayerRepeatStateTypeId, "One"); - } else if (repeatMode == Heos::HeosRepeatMode::Off) { + } else if (repeatMode == REPEAT_MODE_OFF) { device->setStateValue(heosPlayerRepeatStateTypeId, "None"); } break; @@ -600,11 +613,7 @@ void DevicePluginDenon::onHeosRepeatModeReceived(int playerId, Heos::HeosRepeatM void DevicePluginDenon::onHeosShuffleModeReceived(int playerId, bool shuffle) { foreach(Device *device, myDevices().filterByParam(heosPlayerDevicePlayerIdParamTypeId, playerId)) { - if (shuffle) { - device->setStateValue(heosPlayerMuteStateTypeId, true); - } else { - device->setStateValue(heosPlayerMuteStateTypeId, false); - } + device->setStateValue(heosPlayerMuteStateTypeId, shuffle); break; } } @@ -625,13 +634,85 @@ void DevicePluginDenon::onHeosVolumeStatusReceived(int playerId, int volume) } } -void DevicePluginDenon::onHeosNowPlayingMediaStatusReceived(int playerId, QString source, QString artist, QString album, QString song, QString artwork) +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; } diff --git a/denon/deviceplugindenon.h b/denon/deviceplugindenon.h index fbbeb121..84536d55 100644 --- a/denon/deviceplugindenon.h +++ b/denon/deviceplugindenon.h @@ -62,7 +62,8 @@ private: QHash m_avrConnections; QHash m_heos; - QList m_asyncSetups; + QList m_asyncAvrSetups; + QList m_asyncHeosSetups; QHash m_playerIds; QHash m_discoveredPlayerIds; @@ -73,14 +74,14 @@ private slots: void onPluginTimer(); void onUpnpDiscoveryFinished(); - void onHeosConnectionChanged(); + void onHeosConnectionChanged(bool status); void onHeosPlayerDiscovered(HeosPlayer *heosPlayer); - void onHeosPlayStateReceived(int playerId, Heos::HeosPlayerState state); + void onHeosPlayStateReceived(int playerId, PLAYER_STATE state); void onHeosShuffleModeReceived(int playerId, bool shuffle); - void onHeosRepeatModeReceived(int playerId, Heos::HeosRepeatMode repeatMode); + void onHeosRepeatModeReceived(int playerId, REPEAT_MODE 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); + 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); diff --git a/denon/heos.cpp b/denon/heos.cpp index 01bef7c6..b5d5a31d 100644 --- a/denon/heos.cpp +++ b/denon/heos.cpp @@ -26,6 +26,7 @@ #include #include #include +#include Heos::Heos(const QHostAddress &hostAddress, QObject *parent) : QObject(parent), @@ -39,13 +40,22 @@ Heos::Heos(const QHostAddress &hostAddress, QObject *parent) : connect(m_socket, SIGNAL(error(QAbstractSocket::SocketError)), this, SLOT(onError(QAbstractSocket::SocketError))); } - -void Heos::getPlayers() +Heos::~Heos() { - QByteArray cmd = "heos://player/get_players\r\n"; - m_socket->write(cmd); + 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; @@ -66,6 +76,63 @@ void Heos::sendHeartbeat() 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"; @@ -98,15 +165,15 @@ void Heos::setMute(int playerId, bool state) m_socket->write(cmd); } -void Heos::setPlayerState(int playerId, HeosPlayerState state) +void Heos::setPlayerState(int playerId, PLAYER_STATE state) { QByteArray playerStateQuery; - if (state == HeosPlayerState::Play){ + if (state == PLAYER_STATE_PLAY){ playerStateQuery = "&state=play"; - } else if (state == HeosPlayerState::Pause){ + } else if (state == PLAYER_STATE_PAUSE){ playerStateQuery = "&state=pause"; - } else if (state == HeosPlayerState::Stop){ + } else if (state == PLAYER_STATE_STOP){ playerStateQuery = "&state=stop"; } @@ -122,15 +189,15 @@ void Heos::getPlayerState(int playerId) } -void Heos::setPlayMode(int playerId, HeosRepeatMode repeatMode, bool shuffle) +void Heos::setPlayMode(int playerId, REPEAT_MODE repeatMode, bool shuffle) { QByteArray repeatModeQuery; - if (repeatMode == HeosRepeatMode::Off) { + if (repeatMode == REPEAT_MODE_OFF) { repeatModeQuery = "&repeat=off"; - } else if (repeatMode == HeosRepeatMode::One) { + } else if (repeatMode == REPEAT_MODE_ONE) { repeatModeQuery = "&repeat=on_one"; - } else if (repeatMode == HeosRepeatMode::All) { + } else if (repeatMode == REPEAT_MODE_ALL) { repeatModeQuery = "&repeat=on_all"; } @@ -152,6 +219,39 @@ void Heos::getPlayMode(int playerId) 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"; @@ -166,45 +266,137 @@ void Heos::playPrevious(int playerId) m_socket->write(cmd); } -void Heos::getNowPlayingMedia(int playerId) +void Heos::volumeUp(int playerId, int step) { - QByteArray cmd = "heos://player/get_now_playing_media?pid=" + QVariant(playerId).toByteArray() + "\r\n"; + 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); } -Heos::~Heos() +void Heos::volumeDown(int playerId, int step) { - m_socket->close(); + 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); } -bool Heos::connected() +void Heos::clearQueue(int playerId) { - return m_connected; + QByteArray cmd = "heos://player/clear_queue?pid=" + QVariant(playerId).toByteArray() + "\r\n"; + qCDebug(dcDenon) << "clear queue:" << cmd; + m_socket->write(cmd); } -void Heos::connectHeos() +void Heos::moveQueue(int playerId, int sourcQueueId, int destinationQueueId) { - if (m_socket->state() == QAbstractSocket::ConnectingState) { - return; + 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->connectToHost(m_hostAddress, 1255); + m_socket->write(cmd); } -HeosPlayer *Heos::getPlayer(int playerId) +void Heos::toggleGroupMute(int groupId) { - return m_heosPlayers.value(playerId); + 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(); - setConnected(true); + emit connectionStatusChanged(true); } void Heos::onDisconnected() { - qCDebug(dcDenon()) << "disconnected from" << m_hostAddress.toString(); - setConnected(false); + 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) @@ -214,7 +406,7 @@ void Heos::onError(QAbstractSocket::SocketError socketError) void Heos::readData() { - int playerId; + int playerId = 0; QByteArray data; QJsonParseError error; @@ -270,92 +462,19 @@ void Heos::readData() 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); + 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")) { - HeosPlayerState playState = HeosPlayerState::Stop; + PLAYER_STATE playState = PLAYER_STATE_STOP; if (message.queryItemValue("state").contains("play")) { - playState = HeosPlayerState::Play; + playState = PLAYER_STATE_PLAY; } else if (message.queryItemValue("state").contains("pause")) { - playState = HeosPlayerState::Pause; + playState = PLAYER_STATE_PAUSE; } else if (message.queryItemValue("state").contains("stop")) { - playState = HeosPlayerState::Stop; + playState = PLAYER_STATE_STOP; } emit playStateReceived(playerId, playState); } @@ -389,13 +508,13 @@ void Heos::readData() } emit shuffleModeReceived(playerId, shuffle); - HeosRepeatMode repeatMode = HeosRepeatMode::Off; + REPEAT_MODE repeatMode = REPEAT_MODE_OFF; if (message.queryItemValue("repeat").contains("on_all")){ - repeatMode = HeosRepeatMode::All; + repeatMode = REPEAT_MODE_ALL; } else if (message.queryItemValue("repeat").contains("on_one")){ - repeatMode = HeosRepeatMode::One; + repeatMode = REPEAT_MODE_ONE; } else if (message.queryItemValue("repeat").contains("off")){ - repeatMode = HeosRepeatMode::Off; + repeatMode = REPEAT_MODE_OFF; } emit repeatModeReceived(playerId, repeatMode); } @@ -403,13 +522,13 @@ void Heos::readData() if (command.contains("player_state_changed")) { if (message.hasQueryItem("state")) { - HeosPlayerState playState = HeosPlayerState::Stop; + PLAYER_STATE playState = PLAYER_STATE_STOP; if (message.queryItemValue("state").contains("play")) { - playState = HeosPlayerState::Play; + playState = PLAYER_STATE_PLAY; } else if (message.queryItemValue("state").contains("pause")) { - playState = HeosPlayerState::Pause; + playState = PLAYER_STATE_PAUSE; } else if (message.queryItemValue("state").contains("stop")) { - playState = HeosPlayerState::Stop; + playState = PLAYER_STATE_STOP; } emit playStateReceived(playerId, playState); } @@ -435,13 +554,13 @@ void Heos::readData() if (command.contains("repeat_mode_changed")) { if (message.hasQueryItem("repeat")) { - HeosRepeatMode repeatMode = HeosRepeatMode::Off; + REPEAT_MODE repeatMode = REPEAT_MODE_OFF; if (message.queryItemValue("repeat").contains("on_all")){ - repeatMode = HeosRepeatMode::All; + repeatMode = REPEAT_MODE_ALL; } else if (message.queryItemValue("repeat").contains("on_one")){ - repeatMode = HeosRepeatMode::One; + repeatMode = REPEAT_MODE_ONE; } else if (message.queryItemValue("repeat").contains("off")){ - repeatMode = HeosRepeatMode::Off; + repeatMode = REPEAT_MODE_OFF; } emit repeatModeReceived(playerId, repeatMode); } @@ -467,9 +586,3 @@ void Heos::readData() } } } - -void Heos::setConnected(const bool &connected) -{ - m_connected = connected; - emit connectionStatusChanged(); -} diff --git a/denon/heos.h b/denon/heos.h index 4cdc1755..59e7c052 100644 --- a/denon/heos.h +++ b/denon/heos.h @@ -28,49 +28,74 @@ #include #include "heosplayer.h" +#include "heostypes.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 = nullptr); ~Heos(); + void connectHeos(); void setAddress(QHostAddress address); QHostAddress getAddress(); - bool connected(); - void connectHeos(); - - void getPlayers(); 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 setPlayerState(int playerId, HeosPlayerState state); void getVolume(int playerId); - void setVolume(int playerId, int volume); + void getNowPlayingMedia(int playerId); 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 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 getNowPlayingMedia(int playerId); - void registerForChangeEvents(bool state); - void sendHeartbeat(); + 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_connected = false; bool m_eventRegistered = false; QHostAddress m_hostAddress; QTcpSocket *m_socket = nullptr; @@ -79,14 +104,14 @@ private: signals: void playerDiscovered(HeosPlayer *heosPlayer); - void connectionStatusChanged(); + void connectionStatusChanged(bool status); - void playStateReceived(int playerId, HeosPlayerState state); + void playStateReceived(int playerId, PLAYER_STATE state); void shuffleModeReceived(int playerId, bool shuffle); - void repeatModeReceived(int playerId, HeosRepeatMode repeatMode); + void repeatModeReceived(int playerId, REPEAT_MODE 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); + void nowPlayingMediaStatusReceived(int playerId, SOURCE_ID source, QString artist, QString album, QString Song, QString artwork); private slots: void onConnected(); diff --git a/denon/heosplayer.cpp b/denon/heosplayer.cpp index 7ac95c89..51ed1bcb 100644 --- a/denon/heosplayer.cpp +++ b/denon/heosplayer.cpp @@ -35,7 +35,6 @@ HeosPlayer::HeosPlayer(int playerId, QString name, QString serialNumber, QObject m_serialNumber(serialNumber), m_name(name) { - } QString HeosPlayer::name() 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 From c5bf63b7132094fae24224f12339d7d3dc1e102c Mon Sep 17 00:00:00 2001 From: nymea Date: Fri, 26 Jul 2019 13:46:51 +0200 Subject: [PATCH 10/12] fixed duplicated uuids --- denon/deviceplugindenon.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/denon/deviceplugindenon.json b/denon/deviceplugindenon.json index cce1b92b..6c81af86 100644 --- a/denon/deviceplugindenon.json +++ b/denon/deviceplugindenon.json @@ -16,22 +16,22 @@ "interfaces": ["extendedvolumecontroller", "connectable", "power"], "paramTypes": [ { - "id": "a54b98b4-b78f-41dd-a257-14425c6cf9ab", + "id": "cb6eeeb0-3d75-43b6-8177-b5ac19648557", "name": "ip", - "displayName": "Ip", + "displayName": "IPv4 address", "type" : "QString", "inputType": "IPv4Address" }, { "id": "2e8806cb-f6f3-4e9a-b6ea-0b35f75e61c5", "name": "id", - "displayName": "Id", + "displayName": "ID", "type" : "QString" } ], "stateTypes": [ { - "id": "4d1790bf-28c6-4c1f-8892-ba1a0ef140f5", + "id": "fc1dee8b-8fcc-4ec2-8fe6-6be4f5f47a5c", "name": "connected", "displayName": "connected", "displayNameEvent": "connected changed", @@ -148,7 +148,7 @@ "name": "decreaseVolume", "paramTypes": [ { - "id": "765c7e2a-9eb6-46fc-a880-4e96c81f8d1e", + "id": "1d54fda8-336c-436f-ab2b-e8bd549f830c", "displayName": "Step", "name": "step", "type": "int" @@ -221,7 +221,7 @@ "type" : "QString" }, { - "id": "f796664d-6cb7-4f29-9d05-771968d82a32", + "id": "866e8d6a-953f-4bdc-8d85-8d92e51e8592", "name": "serialNumber", "displayName": "Serial number", "type" : "QString" From 729194231e6e2dbc8da1632a36e41c1f8422376a Mon Sep 17 00:00:00 2001 From: nymea Date: Mon, 16 Sep 2019 20:46:09 +0200 Subject: [PATCH 11/12] added mediaplayer interface --- denon/deviceplugindenon.json | 76 +++++++++++++++++++++--------------- 1 file changed, 44 insertions(+), 32 deletions(-) diff --git a/denon/deviceplugindenon.json b/denon/deviceplugindenon.json index 6c81af86..bbb7b711 100644 --- a/denon/deviceplugindenon.json +++ b/denon/deviceplugindenon.json @@ -71,10 +71,10 @@ "writable": true }, { - "displayName": "channel", + "displayName": "Channel", "id": "f29ffa2c-31d6-4d88-b160-a38288c82ce1", "name": "channel", - "displayNameEvent": "channel changed", + "displayNameEvent": "Channel changed", "displayNameAction": "Set channel", "type": "QString", "writable": true, @@ -200,7 +200,7 @@ "name": "heosPlayer", "displayName": "Heos player", "createMethods": ["auto"], - "interfaces": ["extendedmediacontroller", "extendedvolumecontroller", "mediametadataprovider", "shufflerepeat", "connectable"], + "interfaces": ["mediaplayer", "extendedmediacontroller", "extendedvolumecontroller", "mediametadataprovider", "shufflerepeat", "connectable"], "paramTypes":[ { "id": "89629008-6ad8-4e92-863d-b86e0e012d0b", @@ -231,7 +231,7 @@ { "id": "9a4e527e-057c-4b19-8a02-605cc8349f5e", "name": "connected", - "displayName": "connected", + "displayName": "Connected", "displayNameEvent": "Connected changed", "type": "bool", "defaultValue": false, @@ -252,8 +252,8 @@ "id": "6d4886a1-fa5d-4889-96c5-7a1c206f59be", "name": "volume", "displayName": "Volume", - "displayNameEvent": "volume changed", - "displayNameAction": "set volume", + "displayNameEvent": "Volume changed", + "displayNameAction": "Set volume", "type": "int", "defaultValue": 50, "minValue": 0, @@ -263,9 +263,9 @@ { "id": "6db3b484-4cd4-477b-b822-275865d308db", "name": "playbackStatus", - "displayName": "playback status", - "displayNameEvent": "playback status changed", - "displayNameAction": "set playback status", + "displayName": "Playback status", + "displayNameEvent": "Playback status changed", + "displayNameAction": "Set playback status", "type": "QString", "defaultValue": "Stopped", "possibleValues": ["Playing", "Paused", "Stopped"], @@ -275,9 +275,9 @@ { "id": "4b581237-acf5-4d8f-9e83-9b24e9ac900a", "name": "shuffle", - "displayName": "shuffle", - "displayNameEvent": "shuffle changed", - "displayNameAction": "set shuffle", + "displayName": "Shuffle", + "displayNameEvent": "Shuffle changed", + "displayNameAction": "Set shuffle", "type": "bool", "defaultValue": false, "cached": false, @@ -286,9 +286,9 @@ { "id": "4e60cd17-5845-4351-aa2c-2504610e1532", "name": "repeat", - "displayName": "repeat mode", - "displayNameEvent": "repeat mode changed", - "displayNameAction": "set repeat mode", + "displayName": "Repeat mode", + "displayNameEvent": "Repeat mode changed", + "displayNameAction": "Set repeat mode", "type": "QString", "defaultValue": "None", "possibleValues": ["None", "One", "All"], @@ -298,79 +298,91 @@ { "id": "eee22722-3ee5-48f7-8af8-275dc04b21eb", "name": "source", - "displayName": "source", - "displayNameEvent": "source changed", + "displayName": "Source", + "displayNameEvent": "Source changed", "type": "QString", "defaultValue": "" }, { "id": "0a9183a4-b633-4773-ba7a-f4266895157e", "name": "artist", - "displayName": "artist", - "displayNameEvent": "artist changed", + "displayName": "Artist", + "displayNameEvent": "Artist changed", "type": "QString", "defaultValue": "" }, { "id": "9cd60864-f141-4e03-a85b-357690cad1b8", "name": "collection", - "displayName": "album", - "displayNameEvent": "album changed", + "displayName": "Album", + "displayNameEvent": "Album changed", "type": "QString", "defaultValue": "" }, { "id": "bbeecf30-6feb-48d5-ade3-57b2a4eea05f", "name": "title", - "displayName": "title", - "displayNameEvent": "title changed", + "displayName": "Title", + "displayNameEvent": "Title changed", "type": "QString", "defaultValue": "" }, { "id": "a7f0ba95-383a-4efd-adc5-a36e50a04018", "name": "artwork", - "displayName": "artwork", - "displayNameEvent": "artwork changed", + "displayName": "Artwork", + "displayNameEvent": "Artwork changed", "type": "QString", "defaultValue": "" + }, + { + "id": "c59835ac-ee6e-4e6c-aa20-aeb3501937c5", + "name": "playerType", + "displayName": "Player type", + "displayNameEvent": "Player type changed", + "possibleValues": [ + "audio", + "video" + ], + "type": "QString", + "defaultValue": "audio" } ], "actionTypes": [ { "id": "a718f7e9-0b54-4403-b661-49f7b0d13085", "name": "skipBack", - "displayName": "skip back" + "displayName": "Akip back" }, { "id": "fe42d89f-aaad-4f33-a022-d80bdf3a7b19", "name": "fastRewind", - "displayName": "fast rewind" + "displayName": "Tast rewind" }, { "id": "c4b29c09-e3b3-4843-b6d9-e032f3fc1d78", "name": "stop", - "displayName": "stop" + "displayName": "Stop" }, { "id": "c64964e4-cea0-468a-a9bf-8f69657b74e9", "name": "play", - "displayName": "play" + "displayName": "Play" }, { "id": "21c1cbe6-278f-4688-a65f-6620be1ee5ea", "name": "pause", - "displayName": "pause" + "displayName": "Pause" }, { "id": "60b62e88-c68b-463f-b328-2c5d67a71ca0", "name": "fastForward", - "displayName": "fast forward" + "displayName": "Fast forward" }, { "id": "57697e9c-ce5e-4b8f-b42e-16662829ceb2", "name": "skipNext", - "displayName": "skip next" + "displayName": "Skip next" } ] } From 9881962cb8fc28e64bbad8585545f096729b1dcb Mon Sep 17 00:00:00 2001 From: nymea Date: Tue, 17 Sep 2019 21:17:43 +0200 Subject: [PATCH 12/12] use the mediacontroller instead of the extendedmediacontroller interface --- denon/deviceplugindenon.cpp | 10 ---------- denon/deviceplugindenon.json | 12 +----------- 2 files changed, 1 insertion(+), 21 deletions(-) diff --git a/denon/deviceplugindenon.cpp b/denon/deviceplugindenon.cpp index be3b9c7c..75f719fb 100644 --- a/denon/deviceplugindenon.cpp +++ b/denon/deviceplugindenon.cpp @@ -289,11 +289,6 @@ Device::DeviceError DevicePluginDenon::executeAction(Device *device, const Actio return Device::DeviceErrorNoError; } - if (action.actionTypeId() == heosPlayerFastRewindActionTypeId) { - - return Device::DeviceErrorActionTypeNotFound; - } - if (action.actionTypeId() == heosPlayerStopActionTypeId) { heos->setPlayerState(playerId, PLAYER_STATE_STOP); return Device::DeviceErrorNoError; @@ -309,11 +304,6 @@ Device::DeviceError DevicePluginDenon::executeAction(Device *device, const Actio return Device::DeviceErrorNoError; } - if (action.actionTypeId() == heosPlayerFastForwardActionTypeId) { - - return Device::DeviceErrorActionTypeNotFound; - } - if (action.actionTypeId() == heosPlayerSkipNextActionTypeId) { heos->playNext(playerId); return Device::DeviceErrorNoError; diff --git a/denon/deviceplugindenon.json b/denon/deviceplugindenon.json index bbb7b711..a85e9a9a 100644 --- a/denon/deviceplugindenon.json +++ b/denon/deviceplugindenon.json @@ -200,7 +200,7 @@ "name": "heosPlayer", "displayName": "Heos player", "createMethods": ["auto"], - "interfaces": ["mediaplayer", "extendedmediacontroller", "extendedvolumecontroller", "mediametadataprovider", "shufflerepeat", "connectable"], + "interfaces": ["mediaplayer", "mediacontroller", "extendedvolumecontroller", "mediametadataprovider", "shufflerepeat", "connectable"], "paramTypes":[ { "id": "89629008-6ad8-4e92-863d-b86e0e012d0b", @@ -354,11 +354,6 @@ "name": "skipBack", "displayName": "Akip back" }, - { - "id": "fe42d89f-aaad-4f33-a022-d80bdf3a7b19", - "name": "fastRewind", - "displayName": "Tast rewind" - }, { "id": "c4b29c09-e3b3-4843-b6d9-e032f3fc1d78", "name": "stop", @@ -374,11 +369,6 @@ "name": "pause", "displayName": "Pause" }, - { - "id": "60b62e88-c68b-463f-b328-2c5d67a71ca0", - "name": "fastForward", - "displayName": "Fast forward" - }, { "id": "57697e9c-ce5e-4b8f-b42e-16662829ceb2", "name": "skipNext",