import nymea import time import threading import json import requests import random from xml.sax.saxutils import unescape from zeroconfbrowser import ZeroconfDevice, ZeroconfListener, discover pollTimer = None pollFrequency = 30 def discoverThings(info): if info.thingClassId == receiverThingClassId: logger.log("Discovery started for", info.thingClassId) discoveredIps = findIps() for i in range(0, len(discoveredIps)): deviceIp = discoveredIps[i] rUrl = 'http://' + deviceIp + ':80/YamahaRemoteControl/ctrl' body = 'GetParam' headers = {'Content-Type': 'text/xml', 'Accept': '*/*'} rr = requests.post(rUrl, headers=headers, data=body) pollResponse = rr.text if rr.status_code == requests.codes.ok: logger.log("Device with IP " + deviceIp + " is a supported Yamaha AVR.") # get device info stringIndex1 = pollResponse.find("") stringIndex2 = pollResponse.find("") responseExtract = pollResponse[stringIndex1+11:stringIndex2] systemId = responseExtract logger.log("System ID:", systemId) stringIndex1 = pollResponse.find("") stringIndex2 = pollResponse.find("") responseExtract = pollResponse[stringIndex1+12:stringIndex2] modelType = "Yamaha " + responseExtract # check if device already known exists = False for thing in myThings(): logger.log("Comparing to existing receivers: is %s a receiver?" % (thing.name)) if thing.thingClassId == receiverThingClassId: logger.log("Yes, %s is a receiver." % (thing.name)) if thing.paramValue(receiverThingSerialParamTypeId) == systemId: logger.log("Already have receiver with serial number %s in the system: %s" % (systemId, thing.name)) exists = True else: logger.log("Thing %s doesn't match with found receiver with serial number %s" % (thing.name, systemId)) if exists == False: # Receiver doesn't exist yet, so add it thingDescriptor = nymea.ThingDescriptor(receiverThingClassId, modelType) thingDescriptor.params = [ nymea.Param(receiverThingSerialParamTypeId, systemId) ] info.addDescriptor(thingDescriptor) else: # Receiver already exists, so show it to allow reconfiguration thingDescriptor = nymea.ThingDescriptor(receiverThingClassId, modelType, thingId=thing.id) thingDescriptor.params = [ nymea.Param(receiverThingSerialParamTypeId, systemId) ] info.addDescriptor(thingDescriptor) else: logger.log("Device with IP " + deviceIp + " does not appear to be a supported Yamaha AVR.") info.finish(nymea.ThingErrorNoError) return if info.thingClassId == zoneThingClassId: logger.log("Discovery started for", info.thingClassId) for possibleReceiver in myThings(): logger.log("Looking for existing receivers to add zones: is %s a receiver?" % (possibleReceiver.name)) if possibleReceiver.thingClassId == receiverThingClassId: receiver = possibleReceiver deviceIp = receiver.stateValue(receiverUrlStateTypeId) logger.log("Yes, %s with IP address %s is a receiver, looking for zones." % (receiver.name, deviceIp)) rUrl = 'http://' + deviceIp + ':80/YamahaRemoteControl/ctrl' body = 'GetParam' headers = {'Content-Type': 'text/xml', 'Accept': '*/*'} rr = requests.post(rUrl, headers=headers, data=body) pollResponse = rr.text possibleZones = list(("Zone_2", "Zone_3", "Zone_4")) for zone in possibleZones: stringIndex1 = pollResponse.find("<" + zone + ">") stringIndex2 = pollResponse.find("") zoneFound = int(pollResponse[stringIndex1+8:stringIndex2]) zoneNbr = int(zone[5:6]) stringIndex1 = pollResponse.find("") stringIndex2 = pollResponse.find("") systemId = pollResponse[stringIndex1+11:stringIndex2] if zoneFound == 1: logger.log("Additional zone with number %s found." % (str(zoneNbr))) # test if zone already exists exists = False for possibleZone in myThings(): logger.log("Comparing to existing zones: is %s a zone?" % (possibleZone.name)) if possibleZone.thingClassId == zoneThingClassId: zone = possibleZone logger.log("Yes, %s is a zone." % (possibleZone.name)) if zone.paramValue(zoneThingSerialParamTypeId) == systemId and zone.paramValue(zoneThingZoneIdParamTypeId) == zoneNbr: logger.log("Already have zone with number %s in the system" % (str(zoneNbr))) exists = True else: logger.log("Thing %s doesn't match with found zone with number %s" % (possibleZone.name, str(zoneNbr))) elif possibleZone.thingClassId == receiverThingClassId: logger.log("Yes, %s is a main zone." % (possibleZone.name)) else: logger.log("No, %s is not a zone." % (possibleZone.name)) if exists == False: # Zone doesn't exist yet, so add it zoneName = receiver.name + " Zone " + str(zoneNbr) logger.log("Found new additional zone:", zone, zoneNbr) logger.log("Adding %s to the system with parent:" % (zoneName), receiver.name, receiver.id) thingDescriptor = nymea.ThingDescriptor(zoneThingClassId, zoneName, parentId=receiver.id) thingDescriptor.params = [ nymea.Param(zoneThingSerialParamTypeId, systemId), nymea.Param(zoneThingZoneIdParamTypeId, zoneNbr) ] info.addDescriptor(thingDescriptor) else: # Zone already exists, so show it to allow reconfiguration zoneName = receiver.name + " Zone " + str(zoneNbr) thingDescriptor = nymea.ThingDescriptor(zoneThingClassId, zoneName, thingId=zone.id, parentId=receiver.id) thingDescriptor.params = [ nymea.Param(zoneThingSerialParamTypeId, systemId), nymea.Param(zoneThingZoneIdParamTypeId, zoneNbr) ] info.addDescriptor(thingDescriptor) info.finish(nymea.ThingErrorNoError) return def findIps(): # To do: in future use nymea capabilities: # no need of any external libraries, you can just call "serviceBrowser = hardwareManager.zeroconf.registerServiceBrowser()" # and can then loop over "serviceBrowser.entries"# serviceBrowser = hardwareManager.zeroconf.registerServiceBrowser() # for i in range(0, len(serviceBrowser.entries)): # logger.log(serviceBrowser.entries[i]) # foreach (const ZeroConfServiceEntry &entry, m_serviceBrowser->serviceEntries()) { # if (entry.hostAddress().protocol() == QAbstractSocket::IPv6Protocol && entry.hostAddress().toString().startsWith("fe80")) { # // We don't support link-local ipv6 addresses yet. skip those entries # continue; # } # QString uuid; # foreach (const QString &txt, entry.txt()) { # if (txt.startsWith("uuid")) { # uuid = txt.split("=").last(); # break; # } # } # if (QUuid(uuid) == kodiUuid) { # ipString = entry.hostAddress().toString(); # port = entry.port(); # break; # } # } # for now we use zeroconf (def discover & classes ZeroconfDevice & ZeroconfListener) as borrowed from pyvizio ipList = discover("_http._tcp.local.", 5) logger.log(ipList) discoveredIps = [] for i in range(0, len(ipList)): deviceInfo = ipList[i] if "Yamaha" in deviceInfo.name: discoveredIps.append(deviceInfo.ip) return discoveredIps def setupThing(info): if info.thing.thingClassId == receiverThingClassId: searchSystemId = info.thing.paramValue(receiverThingSerialParamTypeId) logger.log("setupThing called for", info.thing.name, searchSystemId) discoveredIps = findIps() found = False for i in range(0, len(discoveredIps)): deviceIp = discoveredIps[i] rUrl = 'http://' + deviceIp + ':80/YamahaRemoteControl/ctrl' body = 'GetParam' headers = {'Content-Type': 'text/xml', 'Accept': '*/*'} rr = requests.post(rUrl, headers=headers, data=body) pollResponse = rr.text if rr.status_code == requests.codes.ok: logger.log("Device with IP " + deviceIp + " is a supported Yamaha AVR.") # get device info stringIndex1 = pollResponse.find("") stringIndex2 = pollResponse.find("") responseExtract = pollResponse[stringIndex1+11:stringIndex2] systemId = responseExtract logger.log("System ID:", systemId) # check if this is the device with the serial number we're looking for if systemId == searchSystemId: logger.log("Device with IP " + deviceIp + " is the existing device.") found = True info.thing.setStateValue(receiverUrlStateTypeId, deviceIp) rr2 = rr else: logger.log("Device with IP " + deviceIp + " does not appear to be a supported Yamaha AVR.") if found == True: info.thing.setStateValue(receiverConnectedStateTypeId, True) pollReceiver(info.thing) info.finish(nymea.ThingErrorNoError) else: info.thing.setStateValue(receiverConnectedStateTypeId, False) info.finish(nymea.ThingErrorHardwareFailure, "Error connecting to the device in the network.") logger.log("Receiver added:", info.thing.name) # If no poll timer is set up yet, start it now logger.log("Creating pollService") global pollTimer global pollFrequency if pollTimer == None: logger.log("Starting timer @ setupThing") pollTimer = nymea.PluginTimer(pollFrequency, pollService) logger.log("timer interval @ setupThing", pollTimer.interval) else: logger.log("Timer already exists @ setupThing") info.finish(nymea.ThingErrorNoError) return # Setup for the zone if info.thing.thingClassId == zoneThingClassId: logger.log("SetupThing for zone:", info.thing.name) # get parent receiver thing, needed to get deviceIp for possibleParent in myThings(): if possibleParent.id == info.thing.parentId: parentReceiver = possibleParent deviceIp = parentReceiver.stateValue(receiverUrlStateTypeId) zoneId = info.thing.paramValue(zoneThingZoneIdParamTypeId) zone = "Zone_" + str(zoneId) try: pollReceiver(info.thing) logger.log(zone + " added.") info.thing.setStateValue(zoneConnectedStateTypeId, True) except: logger.warn("Error getting zone state"); info.finish(nymea.ThingErrorHardwareFailure, "Unable to set up zone.") info.thing.setStateValue(zoneConnectedStateTypeId, False) return; # set up polling for zone status info.finish(nymea.ThingErrorNoError) return def pollReceiver(info): global pollFrequency if info.thingClassId == zoneThingClassId: # get parent receiver thing, needed to get deviceIp for possibleParent in myThings(): if possibleParent.id == info.parentId: parentReceiver = possibleParent deviceIp = parentReceiver.stateValue(receiverUrlStateTypeId) zoneId = info.paramValue(zoneThingZoneIdParamTypeId) logger.log("polling zone", deviceIp, info.name) bodyStart = '' bodyEnd = '' elif info.thingClassId == receiverThingClassId: deviceIp = info.stateValue(receiverUrlStateTypeId) logger.log("polling receiver", deviceIp, info.name) bodyStart = '' bodyEnd = '' rUrl = 'http://' + deviceIp + ':80/YamahaRemoteControl/ctrl' body = bodyStart + 'GetParam' + bodyEnd headers = {'Content-Type': 'text/xml', 'Accept': '*/*'} try: pr = requests.post(rUrl, headers=headers, data=body) polled = True except: logger.log("Device didn't respond to http request to get basic status", deviceIp, info.name) polled = False logger.log("Temporarily reducing pollfrequency to give the device some rest") pollFrequency = 120 if polled == True: pollResponse = pr.text # add distinction between receiver & zone if info.thingClassId == receiverThingClassId: receiver = info if pr.status_code == requests.codes.ok: receiver.setStateValue(receiverConnectedStateTypeId, True) # Get power state if pollResponse.find("Standby") != -1: receiver.setStateValue(receiverPowerStateTypeId, False) powerState = False elif pollResponse.find("On") != -1: receiver.setStateValue(receiverPowerStateTypeId, True) powerState = True else: logger.log("Power state not found!") # Get sleep state stringIndex1 = pollResponse.find("") stringIndex2 = pollResponse.find("") responseExtract = pollResponse[stringIndex1+7:stringIndex2] receiver.setStateValue(receiverSleepStateTypeId, responseExtract) # Get mute state if pollResponse.find("Off") != -1: receiver.setStateValue(receiverMuteStateTypeId, False) elif pollResponse.find("On") != -1: receiver.setStateValue(receiverMuteStateTypeId, True) else: logger.log("Mute state not found!") # Get pure direct state if pollResponse.find("Off") != -1: receiver.setStateValue(receiverPureDirectStateTypeId, False) elif pollResponse.find("On") != -1: receiver.setStateValue(receiverPureDirectStateTypeId, True) else: logger.log("Pure Direct state not found!") # Get enhancer state if pollResponse.find("Off") != -1: receiver.setStateValue(receiverEnhancerStateTypeId, False) elif pollResponse.find("On") != -1: receiver.setStateValue(receiverEnhancerStateTypeId, True) else: logger.log("Enhancer state not found!") # Get input stringIndex1 = pollResponse.find("") stringIndex2 = pollResponse.find("") inputSource = pollResponse[stringIndex1+18:stringIndex2] receiver.setStateValue(receiverInputSourceStateTypeId, inputSource) videoSources = ["HDMI1","HDMI2","HDMI3","HDMI4","HDMI5","AV1","AV2","AV3","AV4","AV5","AV6","V-AUX"] if inputSource in videoSources: receiver.setStateValue(receiverPlayerTypeStateTypeId, "video") else: receiver.setStateValue(receiverPlayerTypeStateTypeId, "audio") # Get sound program stringIndex1 = pollResponse.find("") stringIndex2 = pollResponse.find("") responseExtract = pollResponse[stringIndex1+15:stringIndex2] receiver.setStateValue(receiverSurroundModeStateTypeId, responseExtract) # Get Cinema DSP 3D state stringIndex1 = pollResponse.find("<_3D_Cinema_DSP>") stringIndex2 = pollResponse.find("") responseExtract = pollResponse[stringIndex1+16:stringIndex2] receiver.setStateValue(receiverCinemaDSP3DStateTypeId, responseExtract) # Get Adaptive DRC state stringIndex1 = pollResponse.find("") stringIndex2 = pollResponse.find("") responseExtract = pollResponse[stringIndex1+14:stringIndex2] receiver.setStateValue(receiverAdaptiveDRCStateTypeId, responseExtract) # Get volume - volume is represented by int in Yamaha API, but shown as double = int/10 in Yamaha UI - this is ignored here as nymea wants volume to be an int stringIndex1 = pollResponse.find("") responseExtract = pollResponse[stringIndex1+18:stringIndex1+30] stringIndex2 = responseExtract.find("") responseExtract = responseExtract[0:stringIndex2] volume = int(responseExtract) receiver.setStateValue(receiverVolumeStateTypeId, volume) # Get bass stringIndex1 = pollResponse.find("") responseExtract = pollResponse[stringIndex1+11:stringIndex1+30] stringIndex2 = responseExtract.find("") responseExtract = responseExtract[0:stringIndex2] bass = int(responseExtract) receiver.setStateValue(receiverBassStateTypeId, bass) # Get treble stringIndex1 = pollResponse.find("") responseExtract = pollResponse[stringIndex1+13:stringIndex1+30] stringIndex2 = responseExtract.find("") responseExtract = responseExtract[0:stringIndex2] treble = int(responseExtract) receiver.setStateValue(receiverTrebleStateTypeId, treble) # Get dialogue level stringIndex1 = pollResponse.find("") stringIndex2 = pollResponse.find("") responseExtract = pollResponse[stringIndex1+14:stringIndex2] dialogueLvl = int(responseExtract) receiver.setStateValue(receiverDialogueLevelStateTypeId, dialogueLvl) # Get dialogue lift stringIndex1 = pollResponse.find("") stringIndex2 = pollResponse.find("") responseExtract = pollResponse[stringIndex1+15:stringIndex2] dialogueLift = int(responseExtract) receiver.setStateValue(receiverDialogueLiftStateTypeId, dialogueLift) # Get subwoofer trim stringIndex1 = pollResponse.find("") responseExtract = pollResponse[stringIndex1+21:stringIndex1+30] stringIndex2 = responseExtract.find("") responseExtract = responseExtract[0:stringIndex2] subTrim = int(responseExtract) receiver.setStateValue(receiverSubwooferTrimStateTypeId, subTrim) # Get player info body = '<' + inputSource + '>GetParam' headers = {'Content-Type': 'text/xml', 'Accept': '*/*'} try: plr = requests.post(rUrl, headers=headers, data=body) polled = True except: logger.log("Device didn't respond to http request to get player status", deviceIp, info.name) polled = False logger.log("Temporarily reducing pollfrequency to give the device some rest") pollFrequency = 120 if polled == True: if plr.status_code == requests.codes.ok and powerState == True: playerResponse = plr.text # Get repeat state stringIndex1 = playerResponse.find("") stringIndex2 = playerResponse.find("") responseExtract = playerResponse[stringIndex1+8:stringIndex2] if responseExtract not in ["None", "One", "All"]: responseExtract = "None" receiver.setStateValue(receiverRepeatStateTypeId, responseExtract) # Get shuffle state stringIndex1 = playerResponse.find("") stringIndex2 = playerResponse.find("") responseExtract = playerResponse[stringIndex1+9:stringIndex2] if responseExtract == "On": shuffleStatus = True else: shuffleStatus = False receiver.setStateValue(receiverShuffleStateTypeId, shuffleStatus) # Get playback state stringIndex1 = playerResponse.find("") stringIndex2 = playerResponse.find("") responseExtract = playerResponse[stringIndex1+15:stringIndex2] if responseExtract == "Play": playStatus = "Playing" pollFrequency = min(10, pollFrequency) elif responseExtract == "Pause": playStatus = "Paused" pollFrequency = min(10, pollFrequency) else: playStatus = "Stopped" pollFrequency = min(30, pollFrequency) receiver.setStateValue(receiverPlaybackStatusStateTypeId, playStatus) # Get meta info stringIndex1 = playerResponse.find("") stringIndex2 = playerResponse.find("") responseExtract = playerResponse[stringIndex1+8:stringIndex2] receiver.setStateValue(receiverArtistStateTypeId, unescape(responseExtract, {"&": "&", "'": "'", """: '"'})) stringIndex1 = playerResponse.find("") stringIndex2 = playerResponse.find("") responseExtract = playerResponse[stringIndex1+7:stringIndex2] receiver.setStateValue(receiverCollectionStateTypeId, unescape(responseExtract, {"&": "&", "'": "'", """: '"'})) stringIndex1 = playerResponse.find("") stringIndex2 = playerResponse.find("") responseExtract = playerResponse[stringIndex1+6:stringIndex2] receiver.setStateValue(receiverTitleStateTypeId, unescape(responseExtract, {"&": "&", "'": "'", """: '"'})) # Get artwork --> Yamaha artwork file type isn't recognized by nymea: browse for external cover art? stringIndex1 = playerResponse.find("") stringIndex2 = playerResponse.find("") responseExtract = playerResponse[stringIndex1+5:stringIndex2] artURL = 'http://' + deviceIp + ':80' + responseExtract receiver.setStateValue(receiverArtworkStateTypeId, artURL) else: # Playing from external source so no info available receiver.setStateValue(receiverRepeatStateTypeId, "None") receiver.setStateValue(receiverShuffleStateTypeId, False) receiver.setStateValue(receiverPlaybackStatusStateTypeId, "Stopped") receiver.setStateValue(receiverArtistStateTypeId, "") receiver.setStateValue(receiverCollectionStateTypeId, "") receiver.setStateValue(receiverTitleStateTypeId, "") receiver.setStateValue(receiverArtworkStateTypeId, "") else: receiver.setStateValue(receiverConnectedStateTypeId, False) elif info.thingClassId == zoneThingClassId: zone = info if pr.status_code == requests.codes.ok: zone.setStateValue(zoneConnectedStateTypeId, True) # Get power state if pollResponse.find("Standby") != -1: zone.setStateValue(zonePowerStateTypeId, False) powerState = False elif pollResponse.find("On") != -1: zone.setStateValue(zonePowerStateTypeId, True) powerState = True else: logger.log("Power state not found!") # Get sleep state stringIndex1 = pollResponse.find("") stringIndex2 = pollResponse.find("") responseExtract = pollResponse[stringIndex1+7:stringIndex2] zone.setStateValue(zoneSleepStateTypeId, responseExtract) # Get mute state if pollResponse.find("Off") != -1: zone.setStateValue(zoneMuteStateTypeId, False) elif pollResponse.find("On") != -1: zone.setStateValue(zoneMuteStateTypeId, True) else: logger.log("Mute state not found!") # Get input stringIndex1 = pollResponse.find("") stringIndex2 = pollResponse.find("") inputSource = pollResponse[stringIndex1+18:stringIndex2] zone.setStateValue(zoneInputSourceStateTypeId, inputSource) videoSources = ["HDMI1","HDMI2","HDMI3","HDMI4","HDMI5","AV1","AV2","AV3","AV4","AV5","AV6","V-AUX"] if inputSource in videoSources: zone.setStateValue(zonePlayerTypeStateTypeId, "video") else: zone.setStateValue(zonePlayerTypeStateTypeId, "audio") # Get volume stringIndex1 = pollResponse.find("") responseExtract = pollResponse[stringIndex1+18:stringIndex1+30] stringIndex2 = responseExtract.find("") responseExtract = responseExtract[0:stringIndex2] volume = int(responseExtract) zone.setStateValue(zoneVolumeStateTypeId, volume) # Get player info body = '<' + inputSource + '>GetParam' headers = {'Content-Type': 'text/xml', 'Accept': '*/*'} try: plr = requests.post(rUrl, headers=headers, data=body) polled = True except: logger.log("Device didn't respond to http request to get player status", deviceIp, info.name) polled = False logger.log("Temporarily reducing pollfrequency to give the device some rest") pollFrequency = 120 if polled == True: if plr.status_code == requests.codes.ok and powerState == True: playerResponse = plr.text # Get repeat state stringIndex1 = playerResponse.find("") stringIndex2 = playerResponse.find("") responseExtract = playerResponse[stringIndex1+8:stringIndex2] if responseExtract not in ["None", "One", "All"]: responseExtract = "None" zone.setStateValue(zoneRepeatStateTypeId, responseExtract) # Get shuffle state stringIndex1 = playerResponse.find("") stringIndex2 = playerResponse.find("") responseExtract = playerResponse[stringIndex1+9:stringIndex2] if responseExtract == "On": shuffleStatus = True else: shuffleStatus = False zone.setStateValue(zoneShuffleStateTypeId, shuffleStatus) # Get playback state stringIndex1 = playerResponse.find("") stringIndex2 = playerResponse.find("") responseExtract = playerResponse[stringIndex1+15:stringIndex2] if responseExtract == "Play": playStatus = "Playing" pollFrequency = min(10, pollFrequency) elif responseExtract == "Pause": playStatus = "Paused" pollFrequency = min(10, pollFrequency) else: playStatus = "Stopped" pollFrequency = min(30, pollFrequency) zone.setStateValue(zonePlaybackStatusStateTypeId, playStatus) # Get meta info stringIndex1 = playerResponse.find("") stringIndex2 = playerResponse.find("") responseExtract = playerResponse[stringIndex1+8:stringIndex2] zone.setStateValue(zoneArtistStateTypeId, responseExtract) stringIndex1 = playerResponse.find("") stringIndex2 = playerResponse.find("") responseExtract = playerResponse[stringIndex1+7:stringIndex2] zone.setStateValue(zoneCollectionStateTypeId, responseExtract) stringIndex1 = playerResponse.find("") stringIndex2 = playerResponse.find("") responseExtract = playerResponse[stringIndex1+6:stringIndex2] zone.setStateValue(zoneTitleStateTypeId, responseExtract) stringIndex1 = playerResponse.find("") stringIndex2 = playerResponse.find("") responseExtract = playerResponse[stringIndex1+5:stringIndex2] artURL = 'http://' + deviceIp + ':80' + responseExtract zone.setStateValue(zoneArtworkStateTypeId, artURL) else: # Playing from external source so no info available zone.setStateValue(zoneRepeatStateTypeId, "None") zone.setStateValue(zoneShuffleStateTypeId, False) zone.setStateValue(zonePlaybackStatusStateTypeId, "Stopped") zone.setStateValue(zoneArtistStateTypeId, "") zone.setStateValue(zoneCollectionStateTypeId, "") zone.setStateValue(zoneTitleStateTypeId, "") zone.setStateValue(zoneArtworkStateTypeId, "") else: zone.setStateValue(zoneConnectedStateTypeId, False) def pollService(): logger.log("pollTimer triggered") global pollTimer global pollFrequency logger.log("adjusting timer interval") pollTimer.interval = pollFrequency logger.log("timer interval @ pollService", pollTimer.interval) pollFrequency = 30 # Poll all receivers we know for thing in myThings(): if thing.thingClassId == receiverThingClassId or thing.thingClassId == zoneThingClassId: pollReceiver(thing) def executeAction(info): pollReceiver(info.thing) if info.thing.thingClassId == zoneThingClassId: # get parent receiver thing, needed to get deviceIp for possibleParent in myThings(): if possibleParent.id == info.thing.parentId: parentReceiver = possibleParent deviceIp = parentReceiver.stateValue(receiverUrlStateTypeId) zoneId = info.thing.paramValue(zoneThingZoneIdParamTypeId) bodyStart = '' bodyEnd = '' source = info.thing.stateValue(zoneInputSourceStateTypeId) powerCheck = info.thing.stateValue(zonePowerStateTypeId) elif info.thing.thingClassId == receiverThingClassId: deviceIp = info.thing.stateValue(receiverUrlStateTypeId) bodyStart = '' bodyEnd = '' source = info.thing.stateValue(receiverInputSourceStateTypeId) powerCheck = info.thing.stateValue(receiverPowerStateTypeId) logger.log("executeAction called for thing", info.thing.name, deviceIp, source, info.actionTypeId, info.params) rUrl = 'http://' + deviceIp + ':80/YamahaRemoteControl/ctrl' headers = {'Content-Type': 'text/xml', 'Accept': '*/*'} # turn receiver/zone on if needed, before executing the action if powerCheck == False and info.actionTypeId != receiverPowerActionTypeId and info.actionTypeId != zonePowerActionTypeId: body = bodyStart + 'On' + bodyEnd rr = requests.post(rUrl, headers=headers, data=body) if info.actionTypeId == receiverIncreaseVolumeActionTypeId or info.actionTypeId == zoneIncreaseVolumeActionTypeId: if info.actionTypeId == receiverIncreaseVolumeActionTypeId: stepsize = info.paramValue(receiverIncreaseVolumeActionStepParamTypeId) else: stepsize = info.paramValue(zoneIncreaseVolumeActionStepParamTypeId) volumeDelta = stepsize * 10 while abs(volumeDelta) >= 5: if volumeDelta >= 50: step = "Up 5 dB" volumeDelta -= 50 elif volumeDelta >= 10: step = "Up 1 dB" volumeDelta -= 10 elif volumeDelta >= 5: step = "Up" volumeDelta -= 5 else: break body = bodyStart + '' + step + '' + bodyEnd pr = requests.post(rUrl, headers=headers, data=body) time.sleep(0.5) pollReceiver(info.thing) info.finish(nymea.ThingErrorNoError) return elif info.actionTypeId == receiverDecreaseVolumeActionTypeId or info.actionTypeId == zoneDecreaseVolumeActionTypeId: if info.actionTypeId == receiverDecreaseVolumeActionTypeId: stepsize = info.paramValue(receiverDecreaseVolumeActionStepParamTypeId) else: stepsize = info.paramValue(zoneDecreaseVolumeActionStepParamTypeId) volumeDelta = stepsize * -10 while abs(volumeDelta) >= 5: if volumeDelta <= -50: step = "Down 5 dB" volumeDelta += 50 elif volumeDelta <= -10: step = "Down 1 dB" volumeDelta += 10 elif volumeDelta <= -5: step = "Down" volumeDelta += 5 else: break body = bodyStart + '' + step + '' + bodyEnd pr = requests.post(rUrl, headers=headers, data=body) time.sleep(0.5) pollReceiver(info.thing) info.finish(nymea.ThingErrorNoError) return elif info.actionTypeId == receiverSkipBackActionTypeId or info.actionTypeId == zoneSkipBackActionTypeId: body = '<' + source + '>Skip Rev' rr = requests.post(rUrl, headers=headers, data=body) time.sleep(0.5) pollReceiver(info.thing) # AirPlay statusupdates appear to take a while longer to be available in API if source == "AirPlay": time.sleep(6) pollReceiver(info.thing) info.finish(nymea.ThingErrorNoError) return elif info.actionTypeId == receiverStopActionTypeId or info.actionTypeId == zoneStopActionTypeId: body = '<' + source + '>Stop' rr = requests.post(rUrl, headers=headers, data=body) time.sleep(0.5) pollReceiver(info.thing) if source == "AirPlay": time.sleep(6) pollReceiver(info.thing) info.finish(nymea.ThingErrorNoError) return elif info.actionTypeId == receiverPlayActionTypeId or info.actionTypeId == zonePlayActionTypeId: body = '<' + source + '>Play' rr = requests.post(rUrl, headers=headers, data=body) time.sleep(0.5) pollReceiver(info.thing) if source == "AirPlay": time.sleep(6) pollReceiver(info.thing) info.finish(nymea.ThingErrorNoError) return elif info.actionTypeId == receiverPauseActionTypeId or info.actionTypeId == zonePauseActionTypeId: body = '<' + source + '>Pause' rr = requests.post(rUrl, headers=headers, data=body) time.sleep(0.5) pollReceiver(info.thing) if source == "AirPlay": time.sleep(6) pollReceiver(info.thing) info.finish(nymea.ThingErrorNoError) return elif info.actionTypeId == receiverSkipNextActionTypeId or info.actionTypeId == zoneSkipNextActionTypeId: body = '<' + source + '>Skip Fwd' rr = requests.post(rUrl, headers=headers, data=body) time.sleep(0.5) pollReceiver(info.thing) if source == "AirPlay": time.sleep(6) pollReceiver(info.thing) info.finish(nymea.ThingErrorNoError) return elif info.actionTypeId == receiverPowerActionTypeId or info.actionTypeId == zonePowerActionTypeId: if info.actionTypeId == receiverPowerActionTypeId: power = info.paramValue(receiverPowerActionPowerParamTypeId) else: power = info.paramValue(zonePowerActionPowerParamTypeId) if power == True: powerString = "On" else: powerString = "Standby" body = bodyStart + '' + powerString + '' + bodyEnd rr = requests.post(rUrl, headers=headers, data=body) time.sleep(0.5) pollReceiver(info.thing) info.finish(nymea.ThingErrorNoError) return elif info.actionTypeId == receiverSleepActionTypeId or info.actionTypeId == zoneSleepActionTypeId: if info.actionTypeId == receiverSleepActionTypeId: sleepString = info.paramValue(receiverSleepActionSleepParamTypeId) else: sleepString = info.paramValue(zoneSleepActionSleepParamTypeId) body = bodyStart + '' + sleepString + '' + bodyEnd rr = requests.post(rUrl, headers=headers, data=body) time.sleep(0.5) pollReceiver(info.thing) info.finish(nymea.ThingErrorNoError) return elif info.actionTypeId == receiverMuteActionTypeId or info.actionTypeId == zoneMuteActionTypeId: if info.actionTypeId == receiverMuteActionTypeId: mute = info.paramValue(receiverMuteActionMuteParamTypeId) else: mute = info.paramValue(zoneMuteActionMuteParamTypeId) if mute == True: muteString = "On" else: muteString = "Off" body = bodyStart + '' + muteString + '' + bodyEnd rr = requests.post(rUrl, headers=headers, data=body) time.sleep(0.5) pollReceiver(info.thing) info.finish(nymea.ThingErrorNoError) return elif info.actionTypeId == receiverVolumeActionTypeId or info.actionTypeId == zoneVolumeActionTypeId: if info.actionTypeId == receiverVolumeActionTypeId: newVolume = info.paramValue(receiverVolumeActionVolumeParamTypeId) else: newVolume = info.paramValue(zoneVolumeActionVolumeParamTypeId) # volume needs to be multiple of 5 remainder = newVolume % 5 newVolume -= remainder volumeString = str(newVolume) logger.log("Volume set to", newVolume) body = bodyStart + '' + volumeString + '1dB' + bodyEnd pr = requests.post(rUrl, headers=headers, data=body) time.sleep(0.5) pollReceiver(info.thing) info.finish(nymea.ThingErrorNoError) return elif info.actionTypeId == receiverSubwooferTrimActionTypeId: newTrim = info.paramValue(receiverSubwooferTrimActionSubwooferTrimParamTypeId) # trim needs to be multiple of 5 remainder = newTrim % 5 newTrim -= remainder trimString = str(newTrim) logger.log("Subwoofer trim set to", newTrim) body = bodyStart + '' + trimString + '1dB' + bodyEnd pr = requests.post(rUrl, headers=headers, data=body) time.sleep(0.5) pollReceiver(info.thing) info.finish(nymea.ThingErrorNoError) return elif info.actionTypeId == receiverPureDirectActionTypeId: pureDirect = info.paramValue(receiverPureDirectActionPureDirectParamTypeId) if pureDirect == True: PureDirectString = "On" else: PureDirectString = "Off" body = bodyStart + '' + PureDirectString + '' + bodyEnd rr = requests.post(rUrl, headers=headers, data=body) time.sleep(0.5) pollReceiver(info.thing) info.finish(nymea.ThingErrorNoError) return elif info.actionTypeId == receiverEnhancerActionTypeId: enhancer = info.paramValue(receiverEnhancerActionEnhancerParamTypeId) if enhancer == True: enhancerString = "On" else: enhancerString = "Off" body = bodyStart + '' + enhancerString + '' + bodyEnd rr = requests.post(rUrl, headers=headers, data=body) time.sleep(0.5) pollReceiver(info.thing) info.finish(nymea.ThingErrorNoError) return elif info.actionTypeId == receiverDialogueLevelActionTypeId: diaLvl = info.paramValue(receiverDialogueLevelActionDialogueLevelParamTypeId) diaStr = str(diaLvl) body = bodyStart + '' + diaStr + '' + bodyEnd rr = requests.post(rUrl, headers=headers, data=body) time.sleep(0.5) pollReceiver(info.thing) info.finish(nymea.ThingErrorNoError) return elif info.actionTypeId == receiverDialogueLiftActionTypeId: diaLift = info.paramValue(receiverDialogueLiftActionDialogueLiftParamTypeId) diaStr = str(diaLift) body = bodyStart + '' + diaStr + '' + bodyEnd rr = requests.post(rUrl, headers=headers, data=body) time.sleep(0.5) pollReceiver(info.thing) info.finish(nymea.ThingErrorNoError) return elif info.actionTypeId == receiverBassActionTypeId: bass = info.paramValue(receiverBassActionBassParamTypeId) # bass needs to be multiple of 5 remainder = bass % 5 bass -= remainder bassStr = str(bass) logger.log("Bass set to", bassStr) body = bodyStart + '' + bassStr + '1dB' + bodyEnd rr = requests.post(rUrl, headers=headers, data=body) time.sleep(0.5) pollReceiver(info.thing) info.finish(nymea.ThingErrorNoError) return elif info.actionTypeId == receiverTrebleActionTypeId: treble = info.paramValue(receiverTrebleActionTrebleParamTypeId) # treble needs to be multiple of 5 remainder = treble % 5 treble -= remainder trebleStr = str(treble) logger.log("Treble set to", trebleStr) body = bodyStart + '' + trebleStr + '1dB' + bodyEnd rr = requests.post(rUrl, headers=headers, data=body) time.sleep(0.5) pollReceiver(info.thing) info.finish(nymea.ThingErrorNoError) return elif info.actionTypeId == receiverCinemaDSP3DActionTypeId: dsp3D = info.paramValue(receiverCinemaDSP3DActionCinemaDSP3DParamTypeId) logger.log("Cinema DSP 3D set to", dsp3D) body = bodyStart + '<_3D_Cinema_DSP>' + dsp3D + '' + bodyEnd rr = requests.post(rUrl, headers=headers, data=body) time.sleep(0.5) pollReceiver(info.thing) info.finish(nymea.ThingErrorNoError) return elif info.actionTypeId == receiverAdaptiveDRCActionTypeId: adrc = info.paramValue(receiverAdaptiveDRCActionAdaptiveDRCParamTypeId) logger.log("Adaptive DRC set to", adrc) body = bodyStart + '' + adrc + '' + bodyEnd rr = requests.post(rUrl, headers=headers, data=body) time.sleep(0.5) pollReceiver(info.thing) info.finish(nymea.ThingErrorNoError) return elif info.actionTypeId == receiverInputSourceActionTypeId or info.actionTypeId == zoneInputSourceActionTypeId: if info.actionTypeId == receiverInputSourceActionTypeId: inputSource = info.paramValue(receiverInputSourceActionInputSourceParamTypeId) else: inputSource = info.paramValue(zoneInputSourceActionInputSourceParamTypeId) logger.log("Input Source changed to", inputSource) body = bodyStart + '' + inputSource + '' + bodyEnd rr = requests.post(rUrl, headers=headers, data=body) time.sleep(0.5) pollReceiver(info.thing) info.finish(nymea.ThingErrorNoError) return elif info.actionTypeId == receiverSurroundModeActionTypeId: surroundMode = info.paramValue(receiverSurroundModeActionSurroundModeParamTypeId) logger.log("Surround Mode changed to", surroundMode) if surroundMode != "Straight": body = bodyStart + '' + surroundMode + '' + bodyEnd else: body = bodyStart + 'On' + bodyEnd rr = requests.post(rUrl, headers=headers, data=body) time.sleep(0.5) pollReceiver(info.thing) info.finish(nymea.ThingErrorNoError) return elif info.actionTypeId == receiverShuffleActionTypeId or info.actionTypeId == zoneShuffleActionTypeId: if info.actionTypeId == receiverShuffleActionTypeId: shuffle = info.paramValue(receiverShuffleActionShuffleParamTypeId) else: shuffle = info.paramValue(zoneShuffleActionShuffleParamTypeId) if shuffle == True: shuffleString = "On" else: shuffleString = "Off" body = '<' + source + '>' + shuffleString + '' rr = requests.post(rUrl, headers=headers, data=body) time.sleep(0.5) pollReceiver(info.thing) info.finish(nymea.ThingErrorNoError) return elif info.actionTypeId == receiverRepeatActionTypeId or info.actionTypeId == zoneRepeatActionTypeId: if info.actionTypeId == receiverRepeatActionTypeId: repeat = info.paramValue(receiverRepeatActionRepeatParamTypeId) else: repeat = info.paramValue(zoneRepeatActionRepeatParamTypeId) logger.log("Repeat mode:", repeat) if repeat == "All": repeatString = "All" elif repeat == "One": repeatString = "One" else: repeatString = "Off" body = '<' + source + '>' + repeatString + '' rr = requests.post(rUrl, headers=headers, data=body) time.sleep(0.5) pollReceiver(info.thing) info.finish(nymea.ThingErrorNoError) return elif info.actionTypeId == receiverRandomAlbumActionTypeId or info.actionTypeId == zoneRandomAlbumActionTypeId: body = bodyStart + 'SERVER' + bodyEnd rr = requests.post(rUrl, headers=headers, data=body) time.sleep(0.5) playRandomAlbum(rUrl, "SERVER") time.sleep(0.5) pollReceiver(info.thing) info.finish(nymea.ThingErrorNoError) else: logger.log("Action not yet implemented for thing") info.finish(nymea.ThingErrorNoError) return def playRandomAlbum(rUrl, source): # currently source needs to be SERVER # To do: add code to filter out unselectable items if source == "SERVER": browseTree = ["Random", "Music", "By Album", "Random"] logger.log("Playing random album on source " + source) else: browseTree = [] logger.log("Source not supported for this action") # navigate browseTree (first item select random server, then folder "Music", ...) menuLayer = browseInTree(rUrl, source, browseTree) # don't do anything unless browsing to the required menu item succeeded: if menuLayer == len(browseTree)+1 and menuLayer > 0: # play album by selecting first "selectable" line (attribute = "Item") browseResponse, menuLayer = browseMenuReady(rUrl, source) selectable = False line = 1 while selectable == False: itemTxt, itemAttr = readLine(browseResponse, line) if itemAttr == "Item": selectable = True else: line += 1 logger.log("Selecting line %s with label %s" % (line, itemTxt)) selectLine(rUrl, source, line) return def browseInTree(rUrl, source, browseTree): menuLayer = 1 if browseTree == None: #create empty tree browseTree = [] # go up to the main menu level if needed if len(browseTree) > 0: selLayer = 1 browseResponse, menuLayer = browseMenuReady(rUrl, source) while menuLayer > selLayer: menuLevelUp(rUrl, source) browseResponse, menuLayer = browseMenuReady(rUrl, source) # navigate browseTree for i in range (0, len(browseTree)): if browseTree[i] == "Random": currentLine, maxLine = getLineNbrs(browseResponse) selItem = random.randint(1, maxLine) selectLine(rUrl, source, selItem) else: selItem = findLine(rUrl, source, browseTree[i]) if selItem > 0: selectLine(rUrl, source, selItem) else: logger.log("Requested item not found") # set menuLayer in case of error in browsing? browseResponse, menuLayer = browseMenuReady(rUrl, source) logger.log("Returning menuLayer", menuLayer) if menuLayer < len(browseTree)+1: logger.log("Attention, this isn't the requested menuLayer!") return menuLayer def findLine(rUrl, source, searchTxt): # browse menu level: keep going through menu pages (of 8 items per page) until lineTxt is found loop = True selItem = 0 gotoLine(rUrl, source, 1) while loop == True: browseResponse, menuLayer = browseMenuReady(rUrl, source) currentLine, maxLine = getLineNbrs(browseResponse) # read the 8 lines in the current browseResponse page for i in range(1, 9): itemTxt, itemAttr = readLine(browseResponse, i) if itemTxt == searchTxt: selItem = currentLine + i - 1 loop = False if maxLine > currentLine + 7 and loop == True: # end of list not yet reached, go to next page pageDown(rUrl, source) else: # last page, stop loop loop = False return selItem def browseThing(browseResult): # To do: add browse menu action "create shortcut here" as soon as nymea allows browse menu actions? # To do: limit browsing to sources that allow it? zoneOrReceiver = browseResult.thing pollReceiver(zoneOrReceiver) if zoneOrReceiver.thingClassId == zoneThingClassId: # get parent receiver thing, needed to get deviceIp for possibleParent in myThings(): if possibleParent.id == zoneOrReceiver.parentId: parentReceiver = possibleParent zoneId = zoneOrReceiver.paramValue(zoneThingZoneIdParamTypeId) deviceIp = parentReceiver.stateValue(receiverUrlStateTypeId) bodyStart = '' bodyEnd = '' source = zoneOrReceiver.stateValue(zoneInputSourceStateTypeId) playRandomId = zonePlayRandomBrowserItemActionTypeId powerCheck = zoneOrReceiver.stateValue(zonePowerStateTypeId) elif zoneOrReceiver.thingClassId == receiverThingClassId: parentReceiver = zoneOrReceiver deviceIp = zoneOrReceiver.stateValue(receiverUrlStateTypeId) bodyStart = '' bodyEnd = '' source = zoneOrReceiver.stateValue(receiverInputSourceStateTypeId) playRandomId = receiverPlayRandomBrowserItemActionTypeId powerCheck = zoneOrReceiver.stateValue(receiverPowerStateTypeId) rUrl = 'http://' + deviceIp + ':80/YamahaRemoteControl/ctrl' headers = {'Content-Type': 'text/xml', 'Accept': '*/*'} maxItems = 128 # maxItems is used to truncate very long lists, as browsing them is very slow due to the nature of Yamaha's API # the value of maxItems needs to be a multiple of 8 to work correctly with the browseResponse pages that contain 8 lines # and it needs to be browsable within nymea's browseThing timeout, which would be around 264-304 # but it also seems quite easy to overload the device by making to many API calls, so we limit to 128 # (if you want to test this and get stuck, powering off the receiver (not via nymea) should help) browsableSources = ["SERVER", "USB"] if source in browsableSources: logger.log("Source %s is browsable" % (source)) # turn receiver/zone on if needed, before browsing if powerCheck == False: body = bodyStart + 'On' + bodyEnd rr = requests.post(rUrl, headers=headers, data=body) else: logger.log("Source %s is not browsable" % (source)) browseResult.addItem(nymea.BrowserItem("Empty", "Source not browsable", "Non-selectable item", executable=False, disabled=True, icon=nymea.BrowserIconFavorites)) browseResult.finish(nymea.ThingErrorNoError) return if browseResult.itemId == "": # go to first menu layer selType = "BI" selLayer = 1 selItem = 0 selTxt = "Main menu" for i in range(1, 6): if i == 1: shortcutId = receiverSettingsBrowsingShortcut1ParamTypeId labelId = receiverSettingsShortcutLabel1ParamTypeId elif i == 2: shortcutId = receiverSettingsBrowsingShortcut2ParamTypeId labelId = receiverSettingsShortcutLabel2ParamTypeId elif i == 3: shortcutId = receiverSettingsBrowsingShortcut3ParamTypeId labelId = receiverSettingsShortcutLabel3ParamTypeId elif i == 4: shortcutId = receiverSettingsBrowsingShortcut4ParamTypeId labelId = receiverSettingsShortcutLabel4ParamTypeId elif i == 5: shortcutId = receiverSettingsBrowsingShortcut5ParamTypeId labelId = receiverSettingsShortcutLabel5ParamTypeId browseTree = parentReceiver.setting(shortcutId) labelTxt = parentReceiver.setting(labelId) if len(browseTree) > 0 and source == "SERVER": # shortcut is configured, and source needs to be server scLayer = len(browseTree) + 1 subTxt = "Shortcut to " + browseTree treeInfo = "SC-layer-" + str(scLayer) + "-item-" + str(0) + "-" + browseTree browseResult.addItem(nymea.BrowserItem(treeInfo, labelTxt, subTxt, browsable=True, icon=nymea.BrowserIconFavorites)) else: selType, selLayer, selItem, selTxt = splitBrowseItem(browseResult.itemId) # go up to the selected menu level if needed browseResponse, menuLayer = browseMenuReady(rUrl, source) while menuLayer > selLayer: menuLevelUp(rUrl, source) browseResponse, menuLayer = browseMenuReady(rUrl, source) if selType == "BI": selectLine(rUrl, source, selItem) elif selType == "EL": # jump to first line of truncated part of list gotoLine(rUrl, source, selItem) elif selType == "SC": # shortcut, browse shortcut tree browseTree = selTxt.split("/") selLayer = browseInTree(rUrl, source, browseTree) browseResponse, menuLayer = browseMenuReady(rUrl, source) if menuLayer == len(browseTree)+1 and menuLayer > 0: # don't do anything unless browsing to the required menu item succeeded logger.log("Browsing to required menu item succeeded") selLayer = len(browseTree)+1 selItem = 0 selTxt = "Main menu" else: logger.log("Browsing to required menu item unsuccessful") # go up to the selected menu level if needed while menuLayer > selLayer: menuLevelUp(rUrl, source) browseResponse, menuLayer = browseMenuReady(rUrl, source) selLayer = 1 selType = "BI" selItem = 0 selTxt = "Main menu" # browse menu level: keep going through menu pages (of 8 items per page) while last page hasn't been reached loop = True while loop == True: browseResponse, menuLayer = browseMenuReady(rUrl, source) currentLine, maxLine = getLineNbrs(browseResponse) remainder = currentLine % maxItems logger.log("selType", selType, "currentLine", currentLine, "remainder", remainder) # long lists (longer than maxItems) are truncated and can be extended with user action if selType == "BI" and remainder == 1 and currentLine != 1: # truncate list, and create browsable element that will allow user to continue browsing # create info about menu structure (BI = browsable item, EL = extend list in case long list was truncated) treeInfo = "EL-layer-" + str(menuLayer) + "-item-" + str(currentLine) + "-truncated" browseResult.addItem(nymea.BrowserItem(treeInfo, "Continue", "Click to show the next part of this list", browsable=True, icon=nymea.BrowserIconFolder)) # truncate results, stop loop loop = False elif selType == "EL" and remainder == 1 and currentLine != selItem: # truncate list again, and create browsable element that will allow user to continue browsing # create info about menu structure (BI = browsable item, EL = extend list in case long list was truncated) treeInfo = "EL-layer-" + str(menuLayer) + "-item-" + str(currentLine) + "-truncated" browseResult.addItem(nymea.BrowserItem(treeInfo, "Continue", "Click to show the next part of this list", browsable=True, icon=nymea.BrowserIconFolder)) # truncate results, stop loop loop = False else: # read the 8 lines in the current browseResponse page for i in range(1, 9): itemTxt, itemAttr = readLine(browseResponse, i) itemTxtClean = unescape(itemTxt, {"&": "&", "'": "'", """: '"'}) # create info about menu structure (BI = browsable item, EL = extend list in case long list was truncated) treeInfo = "BI-layer-" + str(menuLayer) + "-item-" + str(currentLine+i-1) + "-" + itemTxt if itemAttr == "Container": if source == "SERVER" and menuLayer == 1: # add browserItemAction play random album # change when nymea supports browserItemActions for python plugins: # browseResult.addItem(nymea.BrowserItem(treeInfo, itemTxtClean, browsable=True, icon=nymea.BrowserIconFolder, browserItemActions=playRandomId)) browseResult.addItem(nymea.BrowserItem(treeInfo, itemTxtClean, browsable=True, icon=nymea.BrowserIconFolder)) else: browseResult.addItem(nymea.BrowserItem(treeInfo, itemTxtClean, browsable=True, icon=nymea.BrowserIconFolder)) elif itemAttr == "Item": browseResult.addItem(nymea.BrowserItem(treeInfo, itemTxtClean, executable=True, icon=nymea.BrowserIconMusic)) else: # found unselectable item, indicating end of list, stop loop if len(itemTxt) > 0: browseResult.addItem(nymea.BrowserItem(treeInfo, itemTxt, "Not selectable on this receiver", executable=False, disabled=True, icon=nymea.BrowserIconFavorites)) else: loop = False if maxLine > currentLine + 7 and loop == True: # end of list not yet reached, go to next page pageDown(rUrl, source) else: # last page, stop loop loop = False browseResult.finish(nymea.ThingErrorNoError) return def executeBrowserItem(info): zoneOrReceiver = info.thing pollReceiver(zoneOrReceiver) if zoneOrReceiver.thingClassId == zoneThingClassId: # get parent receiver thing, needed to get deviceIp for possibleParent in myThings(): if possibleParent.id == zoneOrReceiver.parentId: parentReceiver = possibleParent deviceIp = parentReceiver.stateValue(receiverUrlStateTypeId) source = zoneOrReceiver.stateValue(zoneInputSourceStateTypeId) elif zoneOrReceiver.thingClassId == receiverThingClassId: deviceIp = zoneOrReceiver.stateValue(receiverUrlStateTypeId) source = zoneOrReceiver.stateValue(receiverInputSourceStateTypeId) rUrl = 'http://' + deviceIp + ':80/YamahaRemoteControl/ctrl' selType, selLayer, selItem, selTxt = splitBrowseItem(info.itemId) # go up to the selected menu level if needed browseResponse, menuLayer = browseMenuReady(rUrl, source) while menuLayer > selLayer: menuLevelUp(rUrl, source) browseResponse, menuLayer = browseMenuReady(rUrl, source) selectLine(rUrl, source, selItem) info.finish(nymea.ThingErrorNoError) time.sleep(0.5) pollReceiver(zoneOrReceiver) return def executeBrowserItemAction(info): if info.actionTypeId == receiverPlayRandomBrowserItemActionTypeId or info.actionTypeId == zonePlayRandomBrowserItemActionTypeId: if info.thing.thingClassId == zoneThingClassId: # get parent receiver thing, needed to get deviceIp for possibleParent in myThings(): if possibleParent.id == info.thing.parentId: parentReceiver = possibleParent deviceIp = parentReceiver.stateValue(receiverUrlStateTypeId) zoneId = info.thing.paramValue(zoneThingZoneIdParamTypeId) source = info.thing.stateValue(zoneInputSourceStateTypeId) elif info.thing.thingClassId == receiverThingClassId: deviceIp = info.thing.stateValue(receiverUrlStateTypeId) source = info.thing.stateValue(receiverInputSourceStateTypeId) rUrl = 'http://' + deviceIp + ':80/YamahaRemoteControl/ctrl' playRandomAlbum(rUrl, source) return def selectLine(rUrl, source, selItem): if selItem > 0: headers = {'Content-Type': 'text/xml', 'Accept': '*/*'} gotoLine(rUrl, source, 1) browseResponse, menuLayer = browseMenuReady(rUrl, source) currentLine, maxLine = getLineNbrs(browseResponse) while selItem > currentLine + 7: # jump to the list page with the selected line remainder = selItem % 8 if remainder == 0: remainder = 8 jumpBody = '' + str(selItem - remainder + 1) + '' jr = requests.post(rUrl, headers=headers, data=jumpBody) # confirm we got to right page browseResponse, menuLayer = browseMenuReady(rUrl, source) currentLine, maxLine = getLineNbrs(browseResponse) # now select correct line to go to the next menu level selectBody = '<' + source + '>Line_' + str(selItem - currentLine + 1) + '' sr = requests.post(rUrl, headers=headers, data=selectBody) return def pageDown(rUrl, source): # scroll to next page of list headers = {'Content-Type': 'text/xml', 'Accept': '*/*'} scrollBody = '<' + source + '>Down' sr = requests.post(rUrl, headers=headers, data=scrollBody) return def menuLevelUp(rUrl, source): headers = {'Content-Type': 'text/xml', 'Accept': '*/*'} returnBody = '<' + source + '>Return' ur = requests.post(rUrl, headers=headers, data=returnBody) return def readLine(browseResponse, i): lineResult = [] stringIndex1 = browseResponse.find("") stringIndex2 = browseResponse.find("") browseTxt = browseResponse[stringIndex1+8:stringIndex2] stringIndex1 = browseTxt.find("") stringIndex2 = browseTxt.find("") itemTxt = browseTxt[stringIndex1+5:stringIndex2] stringIndex1 = browseTxt.find("") stringIndex2 = browseTxt.find("") itemAttr = browseTxt[stringIndex1+11:stringIndex2] return itemTxt, itemAttr def splitBrowseItem(itemId): splitId = itemId.split("-",5) selType = splitId[0] selLayer = int(splitId[2]) selItem = int(splitId[4]) selTxt = splitId[5] return selType, selLayer, selItem, selTxt def getLineNbrs(browseResponse): stringIndex1 = browseResponse.find("") stringIndex2 = browseResponse.find("") currentLine = int(browseResponse[stringIndex1+14:stringIndex2]) stringIndex1 = browseResponse.find("") stringIndex2 = browseResponse.find("") maxLine = int(browseResponse[stringIndex1+10:stringIndex2]) return currentLine, maxLine def gotoLine(rUrl, source, lineNbr): # e.g. line 1: make sure we are on the first line in the menu before continuing headers = {'Content-Type': 'text/xml', 'Accept': '*/*'} browseBody = '<' + source + '>GetParam' browseResponse, menuLayer = browseMenuReady(rUrl, source) jumpBody = '<' + source + '>' + str(lineNbr) + '' jr = requests.post(rUrl, headers=headers, data=jumpBody) return def browseMenuReady(rUrl, source): # make sure menu status is Ready before sending any further commands, as they may not be processed by the receiver # at same time, return list info as we got it anyway when checking menu status headers = {'Content-Type': 'text/xml', 'Accept': '*/*'} browseBody = '<' + source + '>GetParam' ready = False while ready == False: br = requests.post(rUrl, headers=headers, data=browseBody) browseResponse = br.text stringIndex1 = browseResponse.find("") stringIndex2 = browseResponse.find("") menuStatus = browseResponse[stringIndex1+13:stringIndex2] if menuStatus == "Ready": ready = True stringIndex1 = browseResponse.find("") stringIndex2 = browseResponse.find("") menuLayer = int(browseResponse[stringIndex1+12:stringIndex2]) stringIndex1 = browseResponse.find("") stringIndex2 = browseResponse.find("") menuTitle = browseResponse[stringIndex1+11:stringIndex2] logger.log("Menu layer", menuLayer, "Menu title", menuTitle) else: time.sleep(0.1) return browseResponse, menuLayer def deinit(): global pollTimer # If we started a poll timer, cancel it on shutdown. if pollTimer is not None: pollTimer = None def thingRemoved(thing): global pollTimer logger.log("removeThing called for", thing.name) # Clean up all data related to this thing if len(myThings()) == 0 and pollTimer is not None: pollTimer = None