/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * Copyright 2013 - 2020, nymea GmbH * Contact: contact@nymea.io * * This file is part of nymea. * This project including source code and documentation is protected by * copyright law, and remains the property of nymea GmbH. All rights, including * reproduction, publication, editing and translation, are reserved. The use of * this project is subject to the terms of a license agreement to be concluded * with nymea GmbH in accordance with the terms of use of nymea GmbH, available * under https://nymea.io/license * * GNU Lesser General Public License Usage * Alternatively, this project may be redistributed and/or modified under the * terms of the GNU Lesser General Public License as published by the Free * Software Foundation; version 3. This project is distributed in the hope that * it will be useful, but WITHOUT ANY WARRANTY; without even the implied * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this project. If not, see . * * For any further details and any questions please contact us under * contact@nymea.io or see our FAQ/Licensing Information on * https://nymea.io/license/faq * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ #include "integrationplugindenon.h" #include "plugininfo.h" #include "integrations/thing.h" #include "network/networkaccessmanager.h" #include "network/upnp/upnpdiscovery.h" #include "network/upnp/upnpdiscoveryreply.h" #include "platform/platformzeroconfcontroller.h" #include "network/zeroconf/zeroconfservicebrowser.h" #include "types/mediabrowseritem.h" #include #include #include #include #include #include IntegrationPluginDenon::IntegrationPluginDenon() { } void IntegrationPluginDenon::init() { m_notificationUrl = QUrl(configValue(denonPluginNotificationUrlParamTypeId).toString()); connect(this, &IntegrationPluginDenon::configValueChanged, this, &IntegrationPluginDenon::onPluginConfigurationChanged); m_serviceBrowser = hardwareManager()->zeroConfController()->createServiceBrowser(); connect(m_serviceBrowser, &ZeroConfServiceBrowser::serviceEntryAdded, this, [=](const ZeroConfServiceEntry &entry){ foreach (Thing *thing, myThings().filterByThingClassId(AVRX1000ThingClassId)) { if (entry.txt().contains("am=AVRX1000")) { QString existingId = thing->paramValue(AVRX1000ThingIdParamTypeId).toString(); QString discoveredId = entry.name().split("@").first(); QHostAddress address = entry.hostAddress(); if (existingId == discoveredId && m_avrConnections.contains(thing->id())) { AvrConnection *avrConnection = m_avrConnections.value(thing->id()); avrConnection->setHostAddress(address); } } } }); } void IntegrationPluginDenon::discoverThings(ThingDiscoveryInfo *info) { if (info->thingClassId() == AVRX1000ThingClassId) { if (!hardwareManager()->zeroConfController()->available()) { qCDebug(dcDenon()) << "Error discovering Denon things. Available:" << hardwareManager()->zeroConfController()->available(); info->finish(Thing::ThingErrorHardwareNotAvailable, "Thing discovery not possible"); return; } QStringList discoveredIds; foreach (const ZeroConfServiceEntry &service, m_serviceBrowser->serviceEntries()) { qCDebug(dcDenon()) << "mDNS service entry:" << service; 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); ThingDescriptor thingDescriptor(AVRX1000ThingClassId, name, address); ParamList params; params.append(Param(AVRX1000ThingIdParamTypeId, id)); thingDescriptor.setParams(params); foreach (Thing *existingThing, myThings().filterByThingClassId(AVRX1000ThingClassId)) { if (existingThing->paramValue(AVRX1000ThingIdParamTypeId).toString() == id) { thingDescriptor.setThingId(existingThing->id()); break; } } info->addThingDescriptor(thingDescriptor); } } info->finish(Thing::ThingErrorNoError); } else if (info->thingClassId() == heosThingClassId) { /* * The HEOS products can be discovered using the UPnP SSDP protocol. Through discovery, * the IP address of the HEOS products can be retrieved. Once the IP address is retrieved, * a telnet connection to port 1255 can be opened to access the HEOS CLI and control the HEOS system. * The HEOS product IP address can also be set statically and manually programmed into the control system. * Search target name (ST) in M-SEARCH discovery request is 'urn:schemas-denon-com:thing:ACT-Denon:1'. */ if (!hardwareManager()->upnpDiscovery()->available()) { qCDebug(dcDenon()) << "UPnP discovery not available"; info->finish(Thing::ThingErrorHardwareNotAvailable, "UPnP discovery not possible"); return; } UpnpDiscoveryReply *reply = hardwareManager()->upnpDiscovery()->discoverDevices(); connect(reply, &UpnpDiscoveryReply::finished, reply, &UpnpDiscoveryReply::deleteLater); connect(reply, &UpnpDiscoveryReply::finished, info, [this, reply, info](){ if (reply->error() != UpnpDiscoveryReply::UpnpDiscoveryReplyErrorNoError) { qCWarning(dcDenon()) << "Upnp discovery error" << reply->error(); info->finish(Thing::ThingErrorHardwareFailure, QT_TR_NOOP("UPnP discovery failed.")); return; } m_heosIpAddresses.clear(); foreach (const UpnpDeviceDescriptor &upnpThing, reply->deviceDescriptors()) { if (upnpThing.modelName().contains("HEOS", Qt::CaseSensitivity::CaseInsensitive) && upnpThing.serialNumber() != "0000001") { // child things have serial number 0000001 qCDebug(dcDenon) << "uPnP thing found:" << upnpThing.modelDescription() << upnpThing.friendlyName() << upnpThing.hostAddress().toString() << upnpThing.modelName() << upnpThing.manufacturer() << upnpThing.serialNumber(); m_heosIpAddresses.insert(upnpThing.serialNumber(), upnpThing.hostAddress()); ThingDescriptor descriptor(heosThingClassId, upnpThing.modelName(), upnpThing.serialNumber()); ParamList params; foreach (Thing *existingThing, myThings()) { if (existingThing->paramValue(heosThingSerialNumberParamTypeId).toString().contains(upnpThing.serialNumber(), Qt::CaseSensitivity::CaseInsensitive)) { descriptor.setThingId(existingThing->id()); break; } } params.append(Param(heosThingModelNameParamTypeId, upnpThing.modelName())); params.append(Param(heosThingSerialNumberParamTypeId, upnpThing.serialNumber())); descriptor.setParams(params); info->addThingDescriptor(descriptor); } } info->finish(Thing::ThingErrorNoError); }); return; } else { info->finish(Thing::ThingErrorThingClassNotFound); } } void IntegrationPluginDenon::startPairing(ThingPairingInfo *info) { info->finish(Thing::ThingErrorNoError, QT_TR_NOOP("Please enter your HEOS account credentials. Leave empty if you doesn't have any. Some features like music browsing won't be available.")); } void IntegrationPluginDenon::confirmPairing(ThingPairingInfo *info, const QString &username, const QString &password) { if (info->thingClassId() == heosThingClassId) { if (username.isEmpty()) { //thing connection will be setup without an user account return info->finish(Thing::ThingErrorNoError); } Q_FOREACH(const QString &serialNumber, m_heosIpAddresses.keys()) { if (serialNumber == info->params().paramValue(heosThingSerialNumberParamTypeId).toString()) { ThingId thingId = info->thingId(); Heos *heos = createHeosConnection(m_heosIpAddresses.value(serialNumber)); m_unfinishedHeosConnections.insert(thingId, heos); m_unfinishedHeosPairings.insert(heos, info); connect(heos, &Heos::destroyed, this, [this, thingId, heos] { qCDebug(dcDenon()) << "Heos connection deleted, cleaning up"; m_unfinishedHeosPairings.remove(heos); m_unfinishedHeosConnections.remove(thingId); }); connect(info, &ThingPairingInfo::aborted, this, [heos] { qCDebug(dcDenon()) << "ThingPairingInfo aborted, deleting heos connection"; heos->deleteLater(); }); heos->connectDevice(); heos->setUserAccount(username, password); pluginStorage()->beginGroup(info->thingId().toString()); pluginStorage()->setValue("username", username); pluginStorage()->setValue("password", password); pluginStorage()->endGroup(); } } } } void IntegrationPluginDenon::setupThing(ThingSetupInfo *info) { Thing *thing = info->thing(); if (thing->thingClassId() == AVRX1000ThingClassId) { qCDebug(dcDenon) << "Setup AVR X1000 thing" << thing->name(); if (m_avrConnections.contains(thing->id())) { qCDebug(dcDenon()) << "Setup after reconfiguration, cleaning up ..."; m_avrConnections.take(thing->id())->deleteLater(); } QString id = thing->paramValue(AVRX1000ThingIdParamTypeId).toString(); QHostAddress address = findAvrById(id); if (address.isNull()) { info->finish(Thing::ThingErrorHardwareNotAvailable); return; } AvrConnection *denonConnection = new AvrConnection(address, 23, this); connect(denonConnection, &AvrConnection::connectionStatusChanged, this, &IntegrationPluginDenon::onAvrConnectionChanged); connect(denonConnection, &AvrConnection::socketErrorOccured, this, &IntegrationPluginDenon::onAvrSocketError); connect(denonConnection, &AvrConnection::commandExecuted, this, &IntegrationPluginDenon::onAvrCommandExecuted); connect(denonConnection, &AvrConnection::channelChanged, this, &IntegrationPluginDenon::onAvrChannelChanged); connect(denonConnection, &AvrConnection::powerChanged, this, &IntegrationPluginDenon::onAvrPowerChanged); connect(denonConnection, &AvrConnection::volumeChanged, this, &IntegrationPluginDenon::onAvrVolumeChanged); connect(denonConnection, &AvrConnection::surroundModeChanged, this, &IntegrationPluginDenon::onAvrSurroundModeChanged); connect(denonConnection, &AvrConnection::muteChanged, this, &IntegrationPluginDenon::onAvrMuteChanged); connect(denonConnection, &AvrConnection::artistChanged, this, &IntegrationPluginDenon::onAvrArtistChanged); connect(denonConnection, &AvrConnection::albumChanged, this, &IntegrationPluginDenon::onAvrAlbumChanged); connect(denonConnection, &AvrConnection::songChanged, this, &IntegrationPluginDenon::onAvrSongChanged); connect(denonConnection, &AvrConnection::playBackModeChanged, this, &IntegrationPluginDenon::onAvrPlayBackModeChanged); connect(denonConnection, &AvrConnection::bassLevelChanged, this, &IntegrationPluginDenon::onAvrBassLevelChanged); connect(denonConnection, &AvrConnection::trebleLevelChanged, this, &IntegrationPluginDenon::onAvrTrebleLevelChanged); connect(denonConnection, &AvrConnection::toneControlEnabledChanged, this, &IntegrationPluginDenon::onAvrToneControlEnabledChanged); m_avrConnections.insert(thing->id(), denonConnection); m_asyncAvrSetups.insert(denonConnection, info); // In case the setup is cancelled before we finish it... connect(info, &QObject::destroyed, this, [this, denonConnection]() { m_asyncAvrSetups.remove(denonConnection); }); connect(info, &ThingSetupInfo::aborted, this, [this, thing] () { if (m_avrConnections.contains(thing->id())) { AvrConnection *connection = m_avrConnections.take(thing->id()); connection->deleteLater(); } }); denonConnection->connectDevice(); return; } else if (thing->thingClassId() == heosThingClassId) { qCDebug(dcDenon) << "Setup Heos connection thing" << thing->name(); QString serialnumber = thing->paramValue(heosThingSerialNumberParamTypeId).toString(); if (serialnumber.isEmpty()) { qCWarning(dcDenon) << "Serial number is empty"; info->finish(Thing::ThingErrorInvalidParameter, QT_TR_NOOP("Serial number is not set")); return; } if (m_heosConnections.contains(thing->id())) { qCDebug(dcDenon()) << "Setup after reconfiguration, cleaning up ..."; m_heosConnections.take(thing->id())->deleteLater(); } if (m_unfinishedHeosConnections.contains(thing->id())) { qCDebug(dcDenon()) << "Setup after discovery"; Heos *heos = m_unfinishedHeosConnections.take(thing->id()); m_heosConnections.insert(thing->id(), heos); info->finish(Thing::ThingErrorNoError); } else { qCDebug(dcDenon()) << "Starting Heos discovery"; if (!hardwareManager()->upnpDiscovery()->available()) { qCDebug(dcDenon()) << "UPnP discovery not available"; info->finish(Thing::ThingErrorHardwareNotAvailable, "Discovery not possible"); return; } UpnpDiscoveryReply *reply = hardwareManager()->upnpDiscovery()->discoverDevices(); connect(reply, &UpnpDiscoveryReply::finished, reply, &UpnpDiscoveryReply::deleteLater); connect(reply, &UpnpDiscoveryReply::finished, info, [this, reply, info] { if (reply->error() != UpnpDiscoveryReply::UpnpDiscoveryReplyErrorNoError) { qCWarning(dcDenon()) << "Upnp discovery error" << reply->error(); info->finish(Thing::ThingErrorHardwareFailure, QT_TR_NOOP("Device discovery failed.")); return; } qCDebug(dcDenon()) << "UPnP discovery finished, found" << reply->deviceDescriptors().count() << "uPnP devices"; Q_FOREACH (const UpnpDeviceDescriptor &upnpThing, reply->deviceDescriptors()) { if (upnpThing.modelName().contains("HEOS", Qt::CaseSensitivity::CaseInsensitive)) { QString serialNumber = info->thing()->paramValue(heosThingSerialNumberParamTypeId).toString(); if (serialNumber == upnpThing.serialNumber()) { ThingId thingId = info->thing()->id(); qCDebug(dcDenon()) << "Found Heos device, creating Heos connection"; Heos *heos = createHeosConnection(upnpThing.hostAddress()); m_heosConnections.insert(thingId, heos); m_asyncHeosSetups.insert(heos, info); // In case the setup is cancelled before we finish it... connect(info, &ThingSetupInfo::aborted, heos, &Heos::deleteLater); connect(heos, &Heos::destroyed, this, [thingId, heos, this] { m_asyncHeosSetups.remove(heos); m_heosConnections.remove(thingId); }); heos->connectDevice(); return; } } } qCDebug(dcDenon()) << "Device not found"; info->finish(Thing::ThingErrorHardwareNotAvailable); return; }); } } else if (thing->thingClassId() == heosPlayerThingClassId) { qCDebug(dcDenon) << "Setup Heos player" << thing->name(); Thing *parentThing = myThings().findById(thing->parentId()); if (!parentThing) { qCWarning(dcDenon()) << "Parent thing not found for Heos player" << thing->name(); return; } if (parentThing->setupStatus() == Thing::ThingSetupStatusComplete) { info->finish(Thing::ThingErrorNoError); } else { connect(parentThing, &Thing::setupStatusChanged, info, [info, parentThing] { if (parentThing->setupStatus() == Thing::ThingSetupStatusComplete) { info->finish(Thing::ThingErrorNoError); } }); } } else { info->finish(Thing::ThingErrorThingClassNotFound); } } void IntegrationPluginDenon::thingRemoved(Thing *thing) { qCDebug(dcDenon) << "Delete " << thing->name(); if (thing->thingClassId() == AVRX1000ThingClassId) { if (m_avrConnections.contains(thing->id())) { AvrConnection *avrConnection = m_avrConnections.take(thing->id()); avrConnection->disconnectDevice(); avrConnection->deleteLater(); } } else if (thing->thingClassId() == heosThingClassId) { if (m_heosConnections.contains(thing->id())) { Heos *heos = m_heosConnections.take(thing->id()); heos->deleteLater(); } pluginStorage()->remove(thing->id().toString()); } if (myThings().empty()) { hardwareManager()->pluginTimerManager()->unregisterTimer(m_pluginTimer); m_pluginTimer = nullptr; } } void IntegrationPluginDenon::executeAction(ThingActionInfo *info) { Thing *thing = info->thing(); Action action = info->action(); qCDebug(dcDenon) << "Execute action" << thing->id() << action.params(); if (thing->thingClassId() == AVRX1000ThingClassId) { AvrConnection *avrConnection = m_avrConnections.value(thing->id()); if (action.actionTypeId() == AVRX1000PlayActionTypeId) { QUuid commandId = avrConnection->play(); connect(info, &ThingActionInfo::aborted, [this, commandId] {m_avrPendingActions.remove(commandId);}); m_avrPendingActions.insert(commandId, info); } else if (action.actionTypeId() == AVRX1000PauseActionTypeId) { QUuid commandId = avrConnection->pause(); connect(info, &ThingActionInfo::aborted, [this, commandId] {m_avrPendingActions.remove(commandId);}); m_avrPendingActions.insert(commandId, info); } else if (action.actionTypeId() == AVRX1000StopActionTypeId) { QUuid commandId = avrConnection->stop(); connect(info, &ThingActionInfo::aborted, [this, commandId] {m_avrPendingActions.remove(commandId);}); m_avrPendingActions.insert(commandId, info); } else if (action.actionTypeId() == AVRX1000SkipNextActionTypeId) { QUuid commandId = avrConnection->skipNext(); connect(info, &ThingActionInfo::aborted, [this, commandId] {m_avrPendingActions.remove(commandId);}); m_avrPendingActions.insert(commandId, info); } else if (action.actionTypeId() == AVRX1000SkipBackActionTypeId) { QUuid commandId = avrConnection->skipBack(); connect(info, &ThingActionInfo::aborted, [this, commandId] {m_avrPendingActions.remove(commandId);}); m_avrPendingActions.insert(commandId, info); } else if (action.actionTypeId() == AVRX1000PowerActionTypeId) { bool power = action.param(AVRX1000PowerActionPowerParamTypeId).value().toBool(); QUuid commandId = avrConnection->setPower(power); connect(info, &ThingActionInfo::aborted, [this, commandId] {m_avrPendingActions.remove(commandId);}); m_avrPendingActions.insert(commandId, info); } else if (action.actionTypeId() == AVRX1000VolumeActionTypeId) { int vol = action.param(AVRX1000VolumeActionVolumeParamTypeId).value().toInt(); QUuid commandId = avrConnection->setVolume(vol); connect(info, &ThingActionInfo::aborted, [this, commandId] {m_avrPendingActions.remove(commandId);}); m_avrPendingActions.insert(commandId, info); } else if (action.actionTypeId() == AVRX1000InputSourceActionTypeId) { QByteArray channel = action.param(AVRX1000InputSourceActionInputSourceParamTypeId).value().toByteArray(); QUuid commandId = avrConnection->setChannel(channel); connect(info, &ThingActionInfo::aborted, [this, commandId] {m_avrPendingActions.remove(commandId);}); m_avrPendingActions.insert(commandId, info); } else if (action.actionTypeId() == AVRX1000IncreaseVolumeActionTypeId) { uint step = action.paramValue(AVRX1000IncreaseVolumeActionStepParamTypeId).toUInt(); uint currentVolume = thing->stateValue(AVRX1000VolumeStateTypeId).toUInt(); QUuid commandId = avrConnection->setVolume(qMin(100, currentVolume + step)); connect(info, &ThingActionInfo::aborted, [this, commandId] {m_avrPendingActions.remove(commandId);}); m_avrPendingActions.insert(commandId, info); } else if (action.actionTypeId() == AVRX1000DecreaseVolumeActionTypeId) { uint step = action.paramValue(AVRX1000DecreaseVolumeActionStepParamTypeId).toUInt(); uint currentVolume = thing->stateValue(AVRX1000VolumeStateTypeId).toUInt(); QUuid commandId = avrConnection->setVolume(qMax(0, currentVolume - step)); connect(info, &ThingActionInfo::aborted, [this, commandId] {m_avrPendingActions.remove(commandId);}); m_avrPendingActions.insert(commandId, info); } else if (action.actionTypeId() == AVRX1000SurroundModeActionTypeId) { QByteArray surroundMode = action.param(AVRX1000SurroundModeActionSurroundModeParamTypeId).value().toByteArray(); QUuid commandId = avrConnection->setSurroundMode(surroundMode); connect(info, &ThingActionInfo::aborted, [this, commandId] {m_avrPendingActions.remove(commandId);}); m_avrPendingActions.insert(commandId, info); } else if (action.actionTypeId() == AVRX1000MuteActionTypeId) { bool mute = action.param(AVRX1000MuteActionMuteParamTypeId).value().toBool(); QUuid commandId = avrConnection->setMute(mute); connect(info, &ThingActionInfo::aborted, [this, commandId] {m_avrPendingActions.remove(commandId);}); m_avrPendingActions.insert(commandId, info); } else if (action.actionTypeId() == AVRX1000RepeatActionTypeId) { QString repeatMode = action.param(AVRX1000RepeatActionRepeatParamTypeId).value().toString(); QUuid commandId; if (repeatMode == "One") { commandId = avrConnection->setRepeat(AvrConnection::RepeatModeRepeatOne); } else if (repeatMode == "All") { commandId = avrConnection->setRepeat(AvrConnection::RepeatModeRepeatAll); } else { commandId = avrConnection->setRepeat(AvrConnection::RepeatModeRepeatNone); } connect(info, &ThingActionInfo::aborted, [this, commandId] {m_avrPendingActions.remove(commandId);}); m_avrPendingActions.insert(commandId, info); } else if (action.actionTypeId() == AVRX1000ShuffleActionTypeId) { bool shuffle = action.param(AVRX1000ShuffleActionShuffleParamTypeId).value().toBool(); QUuid commandId = avrConnection->setRandom(shuffle); connect(info, &ThingActionInfo::aborted, [this, commandId] {m_avrPendingActions.remove(commandId);}); m_avrPendingActions.insert(commandId, info); } else if (action.actionTypeId() == AVRX1000PlaybackStatusActionTypeId) { QString playbackStatus = action.param(AVRX1000PlaybackStatusActionPlaybackStatusParamTypeId).value().toString(); QUuid commandId; if (playbackStatus == "Playing") { commandId = avrConnection->play(); } else if (playbackStatus == "Stopped") { commandId = avrConnection->stop(); } else if (playbackStatus == "Paused") { commandId = avrConnection->pause(); } else { qCWarning(dcDenon()) << "Unrecognized playback status" << playbackStatus; return info->finish(Thing::ThingErrorHardwareFailure, "Unrecognized command"); } connect(info, &ThingActionInfo::aborted, [this, commandId] {m_avrPendingActions.remove(commandId);}); m_avrPendingActions.insert(commandId, info); } else if (action.actionTypeId() == AVRX1000ToneControlActionTypeId) { bool enable = action.param(AVRX1000ToneControlActionToneControlParamTypeId).value().toBool(); QUuid commandId = avrConnection->enableToneControl(enable); connect(info, &ThingActionInfo::aborted, [this, commandId] {m_avrPendingActions.remove(commandId);}); m_avrPendingActions.insert(commandId, info); } else if (action.actionTypeId() == AVRX1000BassActionTypeId) { int bass = action.param(AVRX1000BassActionBassParamTypeId).value().toInt(); QUuid commandId = avrConnection->setBassLevel(bass); connect(info, &ThingActionInfo::aborted, [this, commandId] {m_avrPendingActions.remove(commandId);}); m_avrPendingActions.insert(commandId, info); } else if (action.actionTypeId() == AVRX1000TrebleActionTypeId) { int treble = action.param(AVRX1000TrebleActionTrebleParamTypeId).value().toInt(); QUuid commandId = avrConnection->setTrebleLevel(treble); connect(info, &ThingActionInfo::aborted, [this, commandId] {m_avrPendingActions.remove(commandId);}); m_avrPendingActions.insert(commandId, info); } else { qCWarning(dcDenon()) << "ActionType not found" << thing->thingClass().name() << action.actionTypeId() ; return info->finish(Thing::ThingErrorActionTypeNotFound); } } else if (thing->thingClassId() == heosThingClassId) { Heos *heos = m_heosConnections.value(thing->id()); if (action.actionTypeId() == heosRebootActionTypeId) { heos->rebootSpeaker(); return info->finish(Thing::ThingErrorNoError); } else { qCWarning(dcDenon()) << "ActionType not found" << thing->thingClass().name() << action.actionTypeId() ; return info->finish(Thing::ThingErrorActionTypeNotFound); } } else if (thing->thingClassId() == heosPlayerThingClassId) { Thing *heosThing = myThings().findById(thing->parentId()); Heos *heos = m_heosConnections.value(heosThing->id()); int playerId = thing->paramValue(heosPlayerThingPlayerIdParamTypeId).toInt(); if (action.actionTypeId() == heosPlayerAlertActionTypeId) { heos->playUrl(playerId, m_notificationUrl); return info->finish(Thing::ThingErrorNoError); } else if (action.actionTypeId() == heosPlayerVolumeActionTypeId) { int volume = action.param(heosPlayerVolumeActionVolumeParamTypeId).value().toInt(); heos->setVolume(playerId, volume); return info->finish(Thing::ThingErrorNoError); } else if (action.actionTypeId() == heosPlayerMuteActionTypeId) { bool mute = action.param(heosPlayerMuteActionMuteParamTypeId).value().toBool(); heos->setMute(playerId, mute); return info->finish(Thing::ThingErrorNoError); } else if (action.actionTypeId() == heosPlayerPlaybackStatusActionTypeId) { QString playbackStatus = action.param(heosPlayerPlaybackStatusActionPlaybackStatusParamTypeId).value().toString(); if (playbackStatus == "playing") { heos->setPlayerState(playerId, PLAYER_STATE_PLAY); } else if (playbackStatus == "stopping") { heos->setPlayerState(playerId, PLAYER_STATE_STOP); } else if (playbackStatus == "pausing") { heos->setPlayerState(playerId, PLAYER_STATE_PAUSE); } return info->finish(Thing::ThingErrorNoError); } else if (action.actionTypeId() == heosPlayerShuffleActionTypeId) { bool shuffle = action.param(heosPlayerShuffleActionShuffleParamTypeId).value().toBool(); REPEAT_MODE repeatMode = REPEAT_MODE_OFF; if (thing->stateValue(heosPlayerRepeatStateTypeId) == "One") { repeatMode = REPEAT_MODE_ONE; } else if (thing->stateValue(heosPlayerRepeatStateTypeId) == "All") { repeatMode = REPEAT_MODE_ALL; } heos->setPlayMode(playerId, repeatMode, shuffle); return info->finish(Thing::ThingErrorNoError); } else if (action.actionTypeId() == heosPlayerSkipBackActionTypeId) { heos->playPrevious(playerId); return info->finish(Thing::ThingErrorNoError); } else if (action.actionTypeId() == heosPlayerStopActionTypeId) { heos->setPlayerState(playerId, PLAYER_STATE_STOP); return info->finish(Thing::ThingErrorNoError); } else if (action.actionTypeId() == heosPlayerPlayActionTypeId) { heos->setPlayerState(playerId, PLAYER_STATE_PLAY); return info->finish(Thing::ThingErrorNoError); } else if (action.actionTypeId() == heosPlayerPauseActionTypeId) { heos->setPlayerState(playerId, PLAYER_STATE_PAUSE); return info->finish(Thing::ThingErrorNoError); } else if (action.actionTypeId() == heosPlayerSkipNextActionTypeId) { heos->playNext(playerId); return info->finish(Thing::ThingErrorNoError); } else if (action.actionTypeId() == heosPlayerIncreaseVolumeActionTypeId) { heos->volumeUp(playerId, action.param(heosPlayerIncreaseVolumeActionStepParamTypeId).value().toInt()); info->finish(Thing::ThingErrorNoError); return; } else if (action.actionTypeId() == heosPlayerDecreaseVolumeActionTypeId) { heos->volumeUp(playerId, action.param(heosPlayerDecreaseVolumeActionStepParamTypeId).value().toInt()); info->finish(Thing::ThingErrorNoError); return; } else { qCWarning(dcDenon()) << "ActionType not found" << thing->thingClass().name() << action.actionTypeId() ; return info->finish(Thing::ThingErrorActionTypeNotFound); } } else { qCWarning(dcDenon()) << "ThingClass not found" << thing->thingClass().name() << thing->thingClassId() ; return info->finish(Thing::ThingErrorThingClassNotFound); } } void IntegrationPluginDenon::postSetupThing(Thing *thing) { qCDebug(dcDenon()) << "Post setup thing" << thing->name(); if (thing->thingClassId() == AVRX1000ThingClassId) { AvrConnection *avrConnection = m_avrConnections.value(thing->id()); thing->setStateValue(AVRX1000ConnectedStateTypeId, avrConnection->connected()); avrConnection->getPower(); avrConnection->getMute(); avrConnection->getVolume(); avrConnection->getChannel(); avrConnection->getSurroundMode(); avrConnection->getPlayBackInfo(); avrConnection->getBassLevel(); avrConnection->getTrebleLevel(); avrConnection->getToneControl(); } else if (thing->thingClassId() == heosThingClassId) { Heos *heos = m_heosConnections.value(thing->id()); thing->setStateValue(heosConnectedStateTypeId, heos->connected()); heos->getPlayers(); heos->getGroups(); } else if (thing->thingClassId() == heosPlayerThingClassId) { thing->setStateValue(heosPlayerConnectedStateTypeId, true); Thing *heosThing = myThings().findById(thing->parentId()); Heos *heos = m_heosConnections.value(heosThing->id()); int playerId = thing->paramValue(heosPlayerThingPlayerIdParamTypeId).toInt(); heos->getPlayerState(playerId); heos->getPlayMode(playerId); heos->getVolume(playerId); heos->getMute(playerId); heos->getNowPlayingMedia(playerId); } if (!m_pluginTimer) { qCDebug(dcDenon()) << "Creating plugin timer"; m_pluginTimer = hardwareManager()->pluginTimerManager()->registerTimer(60); connect(m_pluginTimer, &PluginTimer::timeout, this, &IntegrationPluginDenon::onPluginTimer); } } void IntegrationPluginDenon::onPluginTimer() { foreach(AvrConnection *avrConnection, m_avrConnections.values()) { if (!avrConnection->connected()) { avrConnection->connectDevice(); } Thing *thing = myThings().findById(m_avrConnections.key(avrConnection)); if (thing->thingClassId() == AVRX1000ThingClassId) { avrConnection->getPower(); avrConnection->getMute(); avrConnection->getVolume(); avrConnection->getChannel(); avrConnection->getSurroundMode(); avrConnection->getPlayBackInfo(); avrConnection->getBassLevel(); avrConnection->getTrebleLevel(); avrConnection->getToneControl(); } } foreach(Thing *thing, myThings().filterByThingClassId(heosThingClassId)) { Heos *heos = m_heosConnections.value(thing->id()); heos->getPlayers(); heos->registerForChangeEvents(true); } } void IntegrationPluginDenon::onAvrConnectionChanged(bool status) { AvrConnection *denonConnection = static_cast(sender()); // if the thing and from the first setup if (m_asyncAvrSetups.contains(denonConnection)) { // and ist connected if (status) { ThingSetupInfo *info = m_asyncAvrSetups.take(denonConnection); info->thing()->setStateValue(AVRX1000ConnectedStateTypeId, true); info->finish(Thing::ThingErrorNoError); } return; } Thing *thing = myThings().findById(m_avrConnections.key(denonConnection)); if (!thing) { qCWarning(dcDenon()) << "Could not find a thing associated to this AVR connection"; return; } if (thing->thingClassId() == AVRX1000ThingClassId) { thing->setStateValue(AVRX1000ConnectedStateTypeId, denonConnection->connected()); if (!status) { QString id = thing->paramValue(AVRX1000ThingIdParamTypeId).toString(); QHostAddress address = findAvrById(id); if (!address.isNull()){ denonConnection->setHostAddress(address); } } } } void IntegrationPluginDenon::onAvrVolumeChanged(int volume) { AvrConnection *denonConnection = static_cast(sender()); Thing *thing = myThings().findById(m_avrConnections.key(denonConnection)); if (!thing) { qCWarning(dcDenon()) << "Could not find a thing associated to this AVR connection"; return; } if (thing->thingClassId() == AVRX1000ThingClassId) { thing->setStateValue(AVRX1000VolumeStateTypeId, volume); } } void IntegrationPluginDenon::onAvrChannelChanged(const QString &channel) { AvrConnection *denonConnection = static_cast(sender()); Thing *thing = myThings().findById(m_avrConnections.key(denonConnection)); if (!thing) return; if (thing->thingClassId() == AVRX1000ThingClassId) { thing->setStateValue(AVRX1000InputSourceStateTypeId, channel); } } void IntegrationPluginDenon::onAvrMuteChanged(bool mute) { AvrConnection *denonConnection = static_cast(sender()); Thing *thing = myThings().findById(m_avrConnections.key(denonConnection)); if (!thing) { qCWarning(dcDenon()) << "Could not find a thing associated to this AVR connection"; return; } if (thing->thingClassId() == AVRX1000ThingClassId) { thing->setStateValue(AVRX1000MuteStateTypeId, mute); } } void IntegrationPluginDenon::onAvrPowerChanged(bool power) { AvrConnection *denonConnection = static_cast(sender()); Thing *thing = myThings().findById(m_avrConnections.key(denonConnection)); if (!thing) return; if (thing->thingClassId() == AVRX1000ThingClassId) { thing->setStateValue(AVRX1000PowerStateTypeId, power); } } void IntegrationPluginDenon::onAvrSurroundModeChanged(const QString &surroundMode) { AvrConnection *denonConnection = static_cast(sender()); Thing *thing = myThings().findById(m_avrConnections.key(denonConnection)); if (!thing){ qCWarning(dcDenon()) << "Could not find a thing associated to this AVR connection"; return; } if (thing->thingClassId() == AVRX1000ThingClassId) { thing->setStateValue(AVRX1000SurroundModeStateTypeId, surroundMode); } } void IntegrationPluginDenon::onAvrSongChanged(const QString &song) { AvrConnection *denonConnection = static_cast(sender()); Thing *thing = myThings().findById(m_avrConnections.key(denonConnection)); if (!thing){ qCWarning(dcDenon()) << "Could not find a thing associated to this AVR connection"; return; } if (thing->thingClassId() == AVRX1000ThingClassId) { thing->setStateValue(AVRX1000TitleStateTypeId, song); } } void IntegrationPluginDenon::onAvrArtistChanged(const QString &artist) { AvrConnection *denonConnection = static_cast(sender()); Thing *thing = myThings().findById(m_avrConnections.key(denonConnection)); if (!thing){ qCWarning(dcDenon()) << "Could not find a thing associated to this AVR connection"; return; } if (thing->thingClassId() == AVRX1000ThingClassId) { thing->setStateValue(AVRX1000ArtistStateTypeId, artist); } } void IntegrationPluginDenon::onAvrAlbumChanged(const QString &album) { AvrConnection *denonConnection = static_cast(sender()); Thing *thing = myThings().findById(m_avrConnections.key(denonConnection)); if (!thing){ qCWarning(dcDenon()) << "Could not find a thing associated to this AVR connection"; return; } if (thing->thingClassId() == AVRX1000ThingClassId) { thing->setStateValue(AVRX1000CollectionStateTypeId, album); } } void IntegrationPluginDenon::onAvrBassLevelChanged(int level) { AvrConnection *denonConnection = static_cast(sender()); Thing *thing = myThings().findById(m_avrConnections.key(denonConnection)); if (!thing){ qCWarning(dcDenon()) << "Could not find a thing associated to this AVR connection"; return; } if (thing->thingClassId() == AVRX1000ThingClassId) { thing->setStateValue(AVRX1000BassStateTypeId, level); } } void IntegrationPluginDenon::onAvrTrebleLevelChanged(int level) { AvrConnection *denonConnection = static_cast(sender()); Thing *thing = myThings().findById(m_avrConnections.key(denonConnection)); if (!thing){ qCWarning(dcDenon()) << "Could not find a thing associated to this AVR connection"; return; } if (thing->thingClassId() == AVRX1000ThingClassId) { thing->setStateValue(AVRX1000TrebleStateTypeId, level); } } void IntegrationPluginDenon::onAvrToneControlEnabledChanged(bool enabled) { AvrConnection *denonConnection = static_cast(sender()); Thing *thing = myThings().findById(m_avrConnections.key(denonConnection)); if (!thing){ qCWarning(dcDenon()) << "Could not find a thing associated to this AVR connection"; return; } if (thing->thingClassId() == AVRX1000ThingClassId) { thing->setStateValue(AVRX1000ToneControlStateTypeId, enabled); } } void IntegrationPluginDenon::onAvrPlayBackModeChanged(AvrConnection::PlayBackMode mode) { AvrConnection *denonConnection = static_cast(sender()); Thing *thing = myThings().findById(m_avrConnections.key(denonConnection)); if (!thing){ qCWarning(dcDenon()) << "Could not find a thing associated to this AVR connection"; return; } if (thing->thingClassId() == AVRX1000ThingClassId) { switch (mode) { case AvrConnection::PlayBackModePlaying: thing->setStateValue(AVRX1000PlaybackStatusStateTypeId, "Playing"); break; case AvrConnection::PlayBackModePaused: thing->setStateValue(AVRX1000PlaybackStatusStateTypeId, "Paused"); break; case AvrConnection::PlayBackModeStopped: thing->setStateValue(AVRX1000PlaybackStatusStateTypeId, "Stopped"); break; } } } void IntegrationPluginDenon::onAvrSocketError() { AvrConnection *avrConnection = static_cast(sender()); // Check if setup running for this thing if (m_asyncAvrSetups.contains(avrConnection)) { ThingSetupInfo *info = m_asyncAvrSetups.take(avrConnection); m_avrConnections.remove(info->thing()->id()); qCWarning(dcDenon()) << "Could not add thing. The setup failed."; info->finish(Thing::ThingErrorHardwareFailure); // Delete the connection, the thing will not be added and // the connection will be created in the next setup avrConnection->deleteLater(); } } void IntegrationPluginDenon::onAvrCommandExecuted(const QUuid &commandId, bool success) { if (m_avrPendingActions.contains(commandId)) { ThingActionInfo *info = m_avrPendingActions.take(commandId); if (success){ if(info->action().actionTypeId() == AVRX1000PlayActionTypeId) { info->thing()->setStateValue(AVRX1000PlaybackStatusStateTypeId, "Playing"); } else if(info->action().actionTypeId() == AVRX1000PauseActionTypeId) { info->thing()->setStateValue(AVRX1000PlaybackStatusStateTypeId, "Paused"); } else if(info->action().actionTypeId() == AVRX1000StopActionTypeId) { info->thing()->setStateValue(AVRX1000PlaybackStatusStateTypeId, "Stopped"); } else if(info->action().actionTypeId() == AVRX1000PlaybackStatusActionTypeId) { info->thing()->setStateValue(AVRX1000PlaybackStatusStateTypeId, info->action().param(AVRX1000PlaybackStatusActionPlaybackStatusParamTypeId).value()); } info->finish(Thing::ThingErrorNoError); } else { info->finish(Thing::ThingErrorHardwareNotAvailable); } } } void IntegrationPluginDenon::onHeosConnectionChanged(bool status) { Heos *heos = static_cast(sender()); heos->registerForChangeEvents(true); if (status) { if (m_asyncHeosSetups.contains(heos)) { ThingSetupInfo *info = m_asyncHeosSetups.take(heos); info->finish(Thing::ThingErrorNoError); } } Thing *thing = myThings().findById(m_heosConnections.key(heos)); if (!thing) return; qCDebug(dcDenon()) << "Heos connection changed" << thing->name(); if (thing->thingClassId() == heosThingClassId) { if (pluginStorage()->childGroups().contains(thing->id().toString())) { pluginStorage()->beginGroup(thing->id().toString()); QString username = pluginStorage()->value("username").toString(); QString password = pluginStorage()->value("password").toString(); pluginStorage()->endGroup(); heos->setUserAccount(username, password); } else { qCWarning(dcDenon()) << "Plugin storage doesn't contain this thingId"; } if (!status) { thing->setStateValue(heosLoggedInStateTypeId, false); thing->setStateValue(heosUserDisplayNameStateTypeId, ""); qCDebug(dcDenon()) << "Starting Heos discovery"; UpnpDiscoveryReply *reply = hardwareManager()->upnpDiscovery()->discoverDevices(); connect(reply, &UpnpDiscoveryReply::finished, reply, &UpnpDiscoveryReply::deleteLater); connect(reply, &UpnpDiscoveryReply::finished, this, &IntegrationPluginDenon::onHeosDiscoveryFinished); } thing->setStateValue(heosConnectedStateTypeId, status); // update connection status for all child things foreach (Thing *playerThing, myThings().filterByParentId(thing->id())) { if (playerThing->thingClassId() == heosPlayerThingClassId) { playerThing->setStateValue(heosPlayerConnectedStateTypeId, status); } } } } void IntegrationPluginDenon::onHeosPlayersChanged() { Heos *heos = static_cast(sender()); heos->getPlayers(); } void IntegrationPluginDenon::onHeosPlayersReceived(QList heosPlayers) { Heos *heos = static_cast(sender()); Thing *thing = myThings().findById(m_heosConnections.key(heos)); if (!thing) { return; } QList heosPlayerDescriptors; if (heosPlayers.isEmpty()) { qCWarning(dcDenon()) << "Received empty player list, this is likely because of a bug in the Heos API, ignoring"; return; } foreach (HeosPlayer *player, heosPlayers) { ThingDescriptor descriptor(heosPlayerThingClassId, player->name(), player->playerModel(), thing->id()); ParamList params; if (!myThings().filterByParam(heosPlayerThingPlayerIdParamTypeId, player->playerId()).isEmpty()) { continue; } params.append(Param(heosPlayerThingModelParamTypeId, player->playerModel())); params.append(Param(heosPlayerThingPlayerIdParamTypeId, player->playerId())); params.append(Param(heosPlayerThingSerialNumberParamTypeId, player->serialNumber())); params.append(Param(heosPlayerThingVersionParamTypeId, player->playerVersion())); descriptor.setParams(params); qCDebug(dcDenon) << "Found new heos player" << player->name(); heosPlayerDescriptors.append(descriptor); } if (!heosPlayerDescriptors.isEmpty()) autoThingsAppeared(heosPlayerDescriptors); foreach(Thing *existingThing, myThings().filterByParentId(thing->id())) { bool playerAvailable = false; int playerId = existingThing->paramValue(heosPlayerThingPlayerIdParamTypeId).toInt(); foreach (HeosPlayer *player, heosPlayers) { if (player->playerId() == playerId) { playerAvailable = true; break; } } if (!playerAvailable) { qCDebug(dcDenon()) << "Heos player vanished, removing" << thing->name(); autoThingDisappeared(existingThing->id()); m_playerBuffer.remove(playerId); } } } void IntegrationPluginDenon::onHeosPlayerInfoRecieved(HeosPlayer *heosPlayer) { qDebug(dcDenon()) << "Heos player info received" << heosPlayer->name() << heosPlayer->playerId() << heosPlayer->groupId(); m_playerBuffer.insert(heosPlayer->playerId(), heosPlayer); } void IntegrationPluginDenon::onHeosPlayStateReceived(int playerId, PLAYER_STATE state) { foreach(Thing *thing, myThings().filterByParam(heosPlayerThingPlayerIdParamTypeId, playerId)) { if (state == PLAYER_STATE_PAUSE) { thing->setStateValue(heosPlayerPlaybackStatusStateTypeId, "Paused"); } else if (state == PLAYER_STATE_PLAY) { thing->setStateValue(heosPlayerPlaybackStatusStateTypeId, "Playing"); } else if (state == PLAYER_STATE_STOP) { thing->setStateValue(heosPlayerPlaybackStatusStateTypeId, "Stopped"); } break; } } void IntegrationPluginDenon::onHeosRepeatModeReceived(int playerId, REPEAT_MODE repeatMode) { foreach(Thing *thing, myThings().filterByParam(heosPlayerThingPlayerIdParamTypeId, playerId)) { if (repeatMode == REPEAT_MODE_ALL) { thing->setStateValue(heosPlayerRepeatStateTypeId, "All"); } else if (repeatMode == REPEAT_MODE_ONE) { thing->setStateValue(heosPlayerRepeatStateTypeId, "One"); } else if (repeatMode == REPEAT_MODE_OFF) { thing->setStateValue(heosPlayerRepeatStateTypeId, "None"); } break; } } void IntegrationPluginDenon::onHeosShuffleModeReceived(int playerId, bool shuffle) { foreach(Thing *thing, myThings().filterByParam(heosPlayerThingPlayerIdParamTypeId, playerId)) { thing->setStateValue(heosPlayerShuffleStateTypeId, shuffle); break; } } void IntegrationPluginDenon::onHeosMuteStatusReceived(int playerId, bool mute) { foreach(Thing *thing, myThings().filterByParam(heosPlayerThingPlayerIdParamTypeId, playerId)) { thing->setStateValue(heosPlayerMuteStateTypeId, mute); break; } } void IntegrationPluginDenon::onHeosVolumeStatusReceived(int playerId, int volume) { foreach(Thing *thing, myThings().filterByParam(heosPlayerThingPlayerIdParamTypeId, playerId)) { thing->setStateValue(heosPlayerVolumeStateTypeId, volume); break; } } void IntegrationPluginDenon::onHeosNowPlayingMediaStatusReceived(int playerId, const QString &sourceId, const QString &artist, const QString &album, const QString &song, const QString &artwork) { Thing *thing = myThings().filterByParam(heosPlayerThingPlayerIdParamTypeId, playerId).first(); if (!thing) return; thing->setStateValue(heosPlayerArtistStateTypeId, artist); thing->setStateValue(heosPlayerTitleStateTypeId, song); thing->setStateValue(heosPlayerArtworkStateTypeId, artwork); thing->setStateValue(heosPlayerCollectionStateTypeId, album); thing->setStateValue(heosPlayerSourceStateTypeId, sourceId); } void IntegrationPluginDenon::onHeosMusicSourcesReceived(quint32 sequenceNumber, QList musicSources) { Q_UNUSED(sequenceNumber) Heos *heos = static_cast(sender()); Thing *thing = myThings().findById(m_heosConnections.key(heos)); if (!thing) { return; } bool loggedIn = thing->stateValue(heosLoggedInStateTypeId).toBool(); if (m_pendingGetSourcesRequest.contains(heos)) { BrowseResult *result = m_pendingGetSourcesRequest.take(heos); foreach(MusicSourceObject source, musicSources) { MediaBrowserItem item; item.setDisplayName(source.name); item.setId("source=" + QString::number(source.sourceId)); item.setExecutable(false); item.setBrowsable(source.available); if (!source.available) { item.setDescription(tr("Service is not available")); } else { item.setDescription(source.serviceUsername); } item.setIcon(BrowserItem::BrowserIconMusic); if (source.name == "Amazon") { item.setMediaIcon(MediaBrowserItem::MediaBrowserIconAmazon); //result->addItem(item); } else if (source.name == "Deezer") { item.setMediaIcon(MediaBrowserItem::MediaBrowserIconDeezer); //result->addItem(item); } else if (source.name == "Napster") { item.setMediaIcon(MediaBrowserItem::MediaBrowserIconNapster); //result->addItem(item); } else if (source.name == "SoundCloud") { item.setMediaIcon(MediaBrowserItem::MediaBrowserIconSoundCloud); //result->addItem(item); } else if (source.name == "Tidal") { item.setMediaIcon(MediaBrowserItem::MediaBrowserIconTidal); //result->addItem(item); } else if (source.name == "TuneIn") { item.setMediaIcon(MediaBrowserItem::MediaBrowserIconTuneIn); item.setBrowsable(true); item.setDescription(source.serviceUsername); result->addItem(item); } else if (source.name == "Local Music") { item.setMediaIcon(MediaBrowserItem::MediaBrowserIconDisk); //result->addItem(item); } else if (source.name == "Playlists") { item.setMediaIcon(MediaBrowserItem::MediaBrowserIconPlaylist); } else if (source.name == "History") { item.setMediaIcon(MediaBrowserItem::MediaBrowserIconRecentlyPlayed); item.setBrowsable(loggedIn); if (!loggedIn) { item.setDescription("Login required"); } else { item.setDescription(source.serviceUsername); } result->addItem(item); } else if (source.name == "AUX Input") { item.setMediaIcon(MediaBrowserItem::MediaBrowserIconAux); //result->addItem(item); } else if (source.name == "Favorites") { item.setIcon(BrowserItem::BrowserIconFavorites); item.setBrowsable(loggedIn); if (!loggedIn) { item.setDescription("Login required"); } else { item.setDescription(source.serviceUsername); } result->addItem(item); } else { item.setThumbnail(source.image_url); } qDebug(dcDenon()) << "Music source received:" << source.name << source.type << source.sourceId << source.image_url; } result->finish(Thing::ThingErrorNoError); } } void IntegrationPluginDenon::onHeosBrowseRequestReceived(quint32 sequenceNumber, const QString &sourceId, const QString &containerId, QList musicSources, QList mediaItems) { Q_UNUSED(sequenceNumber) Heos *heos = static_cast(sender()); Thing *thing = myThings().findById(m_heosConnections.key(heos)); if (!thing) { return; } bool loggedIn = thing->stateValue(heosLoggedInStateTypeId).toBool(); QString identifier; if (containerId.isEmpty()) { identifier = sourceId; } else { identifier = containerId; } if (QUrl(identifier).isValid()) { identifier = QUrl::fromPercentEncoding(identifier.toUtf8()); } if (m_pendingBrowseResult.contains(identifier)) { BrowseResult *result = m_pendingBrowseResult.take(identifier); foreach(MediaObject media, mediaItems) { MediaBrowserItem item; item.setIcon(BrowserItem::BrowserIconMusic); qDebug(dcDenon()) << "Adding Item" << media.name << media.mediaId << media.containerId << media.mediaType; item.setDisplayName(media.name); if (media.mediaType == MEDIA_TYPE_CONTAINER) { item.setId("container=" + media.containerId + "&" + sourceId); } else { item.setId(media.mediaId); } item.setThumbnail(media.imageUrl); item.setExecutable(media.isPlayable); item.setBrowsable(media.isContainer); //item.setActionTypeIds(); m_mediaObjects.insert(item.id(), media); result->addItem(item); } foreach(MusicSourceObject source, musicSources) { MediaBrowserItem item; item.setDisplayName(source.name); qDebug(dcDenon()) << "Adding Item" << source.name << source.sourceId; item.setId("source=" + QString::number(source.sourceId)); item.setIcon(BrowserItem::BrowserIconMusic); item.setExecutable(false); item.setBrowsable(true); if (source.name.contains("Amazon")) { item.setMediaIcon(MediaBrowserItem::MediaBrowserIconAmazon); //result->addItem(item); } else if (source.name == "Deezer") { item.setMediaIcon(MediaBrowserItem::MediaBrowserIconDeezer); //result->addItem(item); } else if (source.name == "Napster") { item.setMediaIcon(MediaBrowserItem::MediaBrowserIconNapster); //result->addItem(item); } else if (source.name == "SoundCloud") { item.setMediaIcon(MediaBrowserItem::MediaBrowserIconSoundCloud); //result->addItem(item); } else if (source.name == "Tidal") { item.setMediaIcon(MediaBrowserItem::MediaBrowserIconTidal); //result->addItem(item); } else if (source.name == "TuneIn") { item.setMediaIcon(MediaBrowserItem::MediaBrowserIconTuneIn); result->addItem(item); } else if (source.name == "Local Music") { item.setMediaIcon(MediaBrowserItem::MediaBrowserIconDisk); //result->addItem(item); } else if (source.name == "Playlists") { item.setMediaIcon(MediaBrowserItem::MediaBrowserIconPlaylist); //result->addItem(item); } else if (source.name == "History") { item.setMediaIcon(MediaBrowserItem::MediaBrowserIconRecentlyPlayed); item.setBrowsable(loggedIn); if (!loggedIn) { item.setDescription("Login required"); } } else if (source.name == "AUX Input") { item.setMediaIcon(MediaBrowserItem::MediaBrowserIconAux); //result->addItem(item); } else if (source.name == "Favorites") { item.setIcon(BrowserItem::BrowserIconFavorites); item.setBrowsable(loggedIn); if (!loggedIn) { item.setDescription("Login required"); } result->addItem(item); } else { item.setThumbnail(source.image_url); } } result->finish(Thing::ThingErrorNoError); } else { qWarning(dcDenon()) << "Pending browser result doesnt recognize" << identifier << m_pendingBrowseResult.keys(); } } void IntegrationPluginDenon::onHeosBrowseErrorReceived(const QString &sourceId, const QString &containerId, int errorId, const QString &errorMessage) { QString identifier; if (containerId.isEmpty()) { identifier = sourceId; } else { identifier = containerId; } if (m_pendingBrowseResult.contains(identifier)) { BrowseResult *result = m_pendingBrowseResult.take(identifier); qWarning(dcDenon) << "Browse error" << errorMessage << errorId; result->finish(Thing::ThingErrorHardwareFailure, errorMessage); } } void IntegrationPluginDenon::onHeosPlayerNowPlayingChanged(int playerId) { Heos *heos = static_cast(sender()); heos->getNowPlayingMedia(playerId); } void IntegrationPluginDenon::onHeosPlayerQueueChanged(int playerId) { Heos *heos = static_cast(sender()); heos->getNowPlayingMedia(playerId); } void IntegrationPluginDenon::onHeosGroupsReceived(QList groups) { m_groupBuffer.clear(); foreach(GroupObject group, groups) { m_groupBuffer.insert(group.groupId, group); } } void IntegrationPluginDenon::onHeosGroupsChanged() { Heos *heos = static_cast(sender()); heos->getGroups(); heos->getPlayers(); } void IntegrationPluginDenon::onHeosUserChanged(bool signedIn, const QString &userName) { Heos *heos = static_cast(sender()); //This is to check if the credentials are correct if (m_unfinishedHeosPairings.contains(heos)) { ThingPairingInfo *info = m_unfinishedHeosPairings.take(heos); if (signedIn) { info->finish(Thing::ThingErrorNoError); } else { info->finish(Thing::ThingErrorAuthenticationFailure, tr("Wrong username or password")); m_unfinishedHeosConnections.remove(info->thingId()); heos->deleteLater(); } } else if (m_heosConnections.values().contains(heos)) { Thing *thing = myThings().findById(m_heosConnections.key(heos)); thing->setStateValue(heosLoggedInStateTypeId, signedIn); thing->setStateValue(heosUserDisplayNameStateTypeId, userName); } else { qCDebug(dcDenon()) << "Unhandled user changed event" << signedIn << userName; } } void IntegrationPluginDenon::onHeosDiscoveryFinished() { UpnpDiscoveryReply *reply = static_cast(sender()); if (reply->error() != UpnpDiscoveryReply::UpnpDiscoveryReplyErrorNoError) { qCWarning(dcDenon()) << "Upnp discovery error" << reply->error(); return; } Q_FOREACH(const UpnpDeviceDescriptor &upnpThing, reply->deviceDescriptors()) { Q_FOREACH(Thing *thing, myThings().filterByThingClassId(heosThingClassId)) { QString serialNumber = thing->paramValue(heosThingSerialNumberParamTypeId).toString(); if (serialNumber == upnpThing.serialNumber()) { Heos *heos = m_heosConnections.value(thing->id()); if (!heos) { qCWarning(dcDenon()) << "On heos discovery, heos connection not found for" << thing->name(); return; } heos->setAddress(upnpThing.hostAddress()); } } } } void IntegrationPluginDenon::onPluginConfigurationChanged(const ParamTypeId ¶mTypeId, const QVariant &value) { qCDebug(dcDenon()) << "Plugin configuration changed"; // Check advanced mode if (paramTypeId == denonPluginNotificationUrlParamTypeId) { qCDebug(dcDenon()) << "Advanced mode" << (value.toBool() ? "enabled." : "disabled."); m_notificationUrl = value.toUrl(); } } void IntegrationPluginDenon::browseThing(BrowseResult *result) { Heos *heos = m_heosConnections.value(result->thing()->parentId()); if (!heos) { result->finish(Thing::ThingErrorHardwareNotAvailable); return; } if (result->itemId().isEmpty()) { qDebug(dcDenon()) << "Browse source"; MediaBrowserItem item; item.setId("type=group"); item.setIcon(BrowserItem::BrowserIcon::BrowserIconPackage); item.setBrowsable(true); item.setExecutable(false); item.setDisplayName("Groups"); result->addItem(item); heos->getMusicSources(); m_pendingGetSourcesRequest.insert(heos, result); connect(result, &QObject::destroyed, this, [this, heos](){m_pendingGetSourcesRequest.remove(heos);}); } QUrlQuery itemQuery(result->itemId()); if (itemQuery.queryItemValue("type") == "group"){ if (itemQuery.hasQueryItem("group")) { //TBD list players in groups } else { qDebug(dcDenon()) << "Browse source" << result->itemId(); int pid = result->thing()->paramValue(heosPlayerThingPlayerIdParamTypeId).toInt(); HeosPlayer *browsingPlayer = m_playerBuffer.value(pid); foreach (GroupObject group, m_groupBuffer) { MediaBrowserItem item; item.setBrowsable(false); item.setExecutable(false); item.setMediaIcon(MediaBrowserItem::MediaBrowserIconNone); item.setIcon(BrowserItem::BrowserIconPackage); item.setDisplayName(group.name); item.setId(result->itemId() + "&" + "group=" + QString::number(group.groupId)); // if player is already part of the group set action type id to unjoin if (browsingPlayer->groupId() == group.groupId) { item.setActionTypeIds(QList() << heosPlayerUnjoinBrowserItemActionTypeId); } else { item.setActionTypeIds(QList() << heosPlayerJoinBrowserItemActionTypeId); } result->addItem(item); } foreach (HeosPlayer *player, m_playerBuffer.values()) { qDebug(dcDenon) << "Adding group item" << player->name(); if (browsingPlayer->playerId() == player->playerId()) { //player is the current browsing thing continue; } if (player->groupId() != -1) { // Dont display players that are already assigned to a group continue; } MediaBrowserItem item; item.setBrowsable(false); item.setExecutable(false); item.setMediaIcon(MediaBrowserItem::MediaBrowserIconMusicLibrary); item.setIcon(BrowserItem::BrowserIconFile); item.setDisplayName(player->name()); item.setId(result->itemId() + "&player=" + QString::number(player->playerId())); item.setActionTypeIds(QList() << heosPlayerJoinBrowserItemActionTypeId); result->addItem(item); } result->finish(Thing::ThingErrorNoError); } } else if (result->itemId().startsWith("source=")){ qDebug(dcDenon()) << "Browse source" << result->itemId(); QString id = result->itemId().remove("source="); heos->browseSource(id); m_pendingBrowseResult.insert(id, result); connect(result, &QObject::destroyed, this, [this, id](){ m_pendingBrowseResult.remove(id);}); } else if (result->itemId().startsWith("container=")){ qDebug(dcDenon()) << "Browse container" << result->itemId(); QStringList values = result->itemId().split("&"); if (values.length() == 2) { QString id = values[0].remove("container="); heos->browseSourceContainers(values[1], id); // URL encoding is needed because some container ids are a URL and their encoding varies. if (QUrl(id).isValid()) { id = QUrl::fromPercentEncoding(id.toUtf8()); } m_pendingBrowseResult.insert(id, result); connect(result, &QObject::destroyed, this, [this, id](){ m_pendingBrowseResult.remove(id);}); } } } void IntegrationPluginDenon::browserItem(BrowserItemResult *result) { Heos *heos = m_heosConnections.value(result->thing()->parentId()); if (!heos) { result->finish(Thing::ThingErrorHardwareNotAvailable); return; } qDebug(dcDenon()) << "Browse item called" << result->itemId(); result->item().setDisplayName("Test name"); if (m_mediaObjects.contains(result->itemId())) { qCDebug(dcDenon()) << "Media Object found" << m_mediaObjects.value(result->itemId()).name; BrowserItem item(result->itemId(), m_mediaObjects.value(result->itemId()).name, false, true); result->finish(item); } else { qCWarning(dcDenon()) << "Media Object not found for itemId" << result->itemId(); result->finish(Thing::ThingErrorItemNotFound, "Item not found"); } } void IntegrationPluginDenon::executeBrowserItem(BrowserActionInfo *info) { Heos *heos = m_heosConnections.value(info->thing()->parentId()); if (!heos) { info->finish(Thing::ThingErrorHardwareNotAvailable); return; } BrowserAction action = info->browserAction(); int playerId = info->thing()->paramValue(heosPlayerThingPlayerIdParamTypeId).toInt(); qDebug(dcDenon()) << "Execute browse item called. Player Id:" << playerId << "Item ID" << action.itemId(); if (m_mediaObjects.contains(action.itemId())) { MediaObject media = m_mediaObjects.value(action.itemId()); if (media.mediaType == MEDIA_TYPE_CONTAINER) { heos->addContainerToQueue(playerId, media.sourceId, media.containerId, ADD_CRITERIA_PLAY_NOW); } else if (media.mediaType == MEDIA_TYPE_STATION) { heos->playStation(playerId, media.sourceId, media.containerId, media.mediaId, media.name); } } else { qWarning(dcDenon()) << "Media item not found" << action.itemId(); } info->finish(Thing::ThingErrorNoError); return; } void IntegrationPluginDenon::executeBrowserItemAction(BrowserItemActionInfo *info) { Heos *heos = m_heosConnections.value(info->thing()->parentId()); if (!heos) { info->finish(Thing::ThingErrorHardwareNotAvailable); return; } QUrlQuery query(info->browserItemAction().itemId()); if (info->browserItemAction().actionTypeId() == heosPlayerJoinBrowserItemActionTypeId) { if (query.hasQueryItem("player")) { QList playerIds; playerIds.append(query.queryItemValue("player").toInt()); playerIds.append(info->thing()->paramValue(heosPlayerThingPlayerIdParamTypeId).toInt()); heos->setGroup(playerIds); } else if(query.hasQueryItem("group")) { GroupObject group = m_groupBuffer.value(query.queryItemValue("group").toInt()); qDebug(dcDenon()) << "Execute browse item action called, Group:" << query.queryItemValue("group").toInt() << group.name; QList playerIds; foreach(PlayerObject player, group.players) { playerIds.append(player.playerId); } playerIds.append(info->thing()->paramValue(heosPlayerThingPlayerIdParamTypeId).toInt()); heos->setGroup(playerIds); } } else if (info->browserItemAction().actionTypeId() == heosPlayerUnjoinBrowserItemActionTypeId) { if(query.hasQueryItem("group")) { GroupObject group = m_groupBuffer.value(query.queryItemValue("group").toInt()); QList playerIds; foreach(PlayerObject player, group.players) { if (player.playerId != info->thing()->paramValue(heosPlayerThingPlayerIdParamTypeId).toInt()) playerIds.append(player.playerId); } heos->setGroup(playerIds); } } info->finish(Thing::ThingErrorNoError); return; } Heos *IntegrationPluginDenon::createHeosConnection(const QHostAddress &address) { Heos *heos = new Heos(address, this); connect(heos, &Heos::connectionStatusChanged, this, &IntegrationPluginDenon::onHeosConnectionChanged); connect(heos, &Heos::playersChanged, this, &IntegrationPluginDenon::onHeosPlayersChanged); connect(heos, &Heos::playersRecieved, this, &IntegrationPluginDenon::onHeosPlayersReceived); connect(heos, &Heos::playerInfoRecieved, this, &IntegrationPluginDenon::onHeosPlayerInfoRecieved); connect(heos, &Heos::playerPlayStateReceived, this, &IntegrationPluginDenon::onHeosPlayStateReceived); connect(heos, &Heos::playerRepeatModeReceived, this, &IntegrationPluginDenon::onHeosRepeatModeReceived); connect(heos, &Heos::playerShuffleModeReceived, this, &IntegrationPluginDenon::onHeosShuffleModeReceived); connect(heos, &Heos::playerMuteStatusReceived, this, &IntegrationPluginDenon::onHeosMuteStatusReceived); connect(heos, &Heos::playerVolumeReceived, this, &IntegrationPluginDenon::onHeosVolumeStatusReceived); connect(heos, &Heos::nowPlayingMediaStatusReceived, this, &IntegrationPluginDenon::onHeosNowPlayingMediaStatusReceived); connect(heos, &Heos::playerNowPlayingChanged, this, &IntegrationPluginDenon::onHeosPlayerNowPlayingChanged); connect(heos, &Heos::musicSourcesReceived, this, &IntegrationPluginDenon::onHeosMusicSourcesReceived); connect(heos, &Heos::browseRequestReceived, this, &IntegrationPluginDenon::onHeosBrowseRequestReceived); connect(heos, &Heos::browseErrorReceived, this, &IntegrationPluginDenon::onHeosBrowseErrorReceived); connect(heos, &Heos::playerQueueChanged, this, &IntegrationPluginDenon::onHeosPlayerQueueChanged); connect(heos, &Heos::groupsReceived, this, &IntegrationPluginDenon::onHeosGroupsReceived); connect(heos, &Heos::groupsChanged, this, &IntegrationPluginDenon::onHeosGroupsChanged); connect(heos, &Heos::userChanged, this, &IntegrationPluginDenon::onHeosUserChanged); return heos; } QHostAddress IntegrationPluginDenon::findAvrById(const QString &id) { foreach (const ZeroConfServiceEntry &service, m_serviceBrowser->serviceEntries()) { if (service.txt().contains("am=AVRX1000")) { if (service.name().split("@").first() == id) { return service.hostAddress(); } } } return QHostAddress(); }