Plex Webhooks

@ckronengold I am using the correct device to monitor. I am getting JSON events from the Apple TV Plex App. They just don't trigger anything. I just realized that maybe I am using some older code. Is there a link somewhere to the most recent release perhaps so I can compare versions?

@jebbett So I figured out what the issue is. It seems like it's related to using multiple MediaScene rooms. I wiped out all the apps and drivers and started from scratch with everything. As long as the Apple TV is the first device to add to the Media Scene app, everything works. If you start with another device/room and then try to add a second Room or third device to monitor, I get an error even though the Media Scene room gets created. Only the first room created works.

When clicking Done to create the second Room, I get

"Unexpected Error

An unexpected error has occurred trying to load the app. Check [Logs] for more information."

There is nothing in the logs to show why this error pops up.

I was having the same thing happen when adding 4 rooms recently. They seem to be working properly in the limited testing I did, but the errors were surprising.

1 Like

Thanks for confirming this. I thought I was losing my mind today lol

I've fixed it, stupid error but no idea how the same error wasn't triggered when opening the main app..

MediaScene:
==================

PARENT APP

/**
 *  Media Scene
 *
 *  Copyright 2018 Jake Tebbett
 *
 *  Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
 *  in compliance with the License. You may obtain a copy of the License at:
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
 *  on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
 *  for the specific language governing permissions and limitations under the License.
 * 
 * VERSION CONTROL - Media Scene
 * ###############
 *
 *  v1.0 - Initial Release
 *
 */

definition(
    name: "MediaScene",
    namespace: "jebbett",
    author: "Jake Tebbett",
    description: "Control scenes based on media state and type",
    category: "My Apps",
    iconUrl: "https://github.com/jebbett/MediaScene/raw/master/Icons/MediaScene.png",
    iconX2Url: "https://github.com/jebbett/MediaScene/raw/master/Icons/MediaScene.png",
    iconX3Url: "https://github.com/jebbett/MediaScene/raw/master/Icons/MediaScene.png"
)

def installed() {
	state.installedOK = true
    initialize()
}

def updated() {
    unsubscribe()
    initialize()
}

def initialize() {
}

preferences {
    page name: "installedCheck"
	page name: "installPage"
	page name: "mainMenu"
}

def installedCheck(){
	if (state?.installedOK){
		mainMenu()
	}else{
		installPage()
	}
}

def installPage() {
    dynamicPage(name: "installPage", title: "Rooms", install: true, uninstall: false, submitOnChange: true) {              
        section(""){
            paragraph "<h1><img src='https://github.com/jebbett/MediaScene/raw/master/Icons/MediaScene.png' width='64' height='64' />&nbsp;<strong><span style='color: #ff6600;'>MediaScene</span></strong></h1>"
			paragraph "Press 'Done' to install..."
        }
    }
}

def mainMenu() {
    dynamicPage(name: "mainMenu", install: true, uninstall: true, submitOnChange: true) {              
        section(""){
            paragraph "<h1><img src='https://github.com/jebbett/MediaScene/raw/master/Icons/MediaScene.png' width='64' height='64' />&nbsp;<strong><span style='color: #ff6600;'>MediaScene</span></strong></h1>"
			paragraph "<b>MediaScene allows you to control devices based on the playback state of a media device</b>"
			paragraph "This can be used with devices created by Plex Communicator or other media devices"
        }
		section {
            app(name: "childapp", appName: "MediaSceneChild", namespace: "jebbett", title: "Create New Room", multiple: true)
    	}
        section {
        	input(name: "debugLogging", type: "bool", title: "Enable Debugging", required: false)
        }
    }
}

CHILD APP

/**
 *  Media Scene
 *
 *  Copyright 2018 Jake Tebbett
 *
 *  Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
 *  in compliance with the License. You may obtain a copy of the License at:
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
 *  on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
 *  for the specific language governing permissions and limitations under the License.
 * 
 * VERSION CONTROL - Media Scene Child
 * ###############
 *
 *  v1.0 - Initial Release
 *  v1.1  - Removed Routines
 */

definition(
    name: "MediaSceneChild",
    namespace: "jebbett",
    author: "Jake Tebbett",
    description: "Control scenes based on media state and type",
    category: "My Apps",
    parent: "jebbett:MediaScene",
    iconUrl: "https://github.com/jebbett/MediaScene/raw/master/Icons/MediaScene.png",
    iconX2Url: "https://github.com/jebbett/MediaScene/raw/master/Icons/MediaScene.png",
    iconX3Url: "https://github.com/jebbett/MediaScene/raw/master/Icons/MediaScene.png"
)


def installed() {
    initialize()
}

def updated() {
    unsubscribe()
    initialize()
}

def initialize() {
  	state.catcherRunning = false
    subscribe(playerDT, "status", PlayerDTCommandRecieved)
}

preferences {
    page name: "childPage"
    //Child Pages
    page name: "pageDoThis"
    page name: "pageWhenThis"
    page name: "pageMediaSettings"
}


def childPage() {
    dynamicPage(name: "childPage", uninstall: true, install: true) {
        section() {
                label title: "<b>Room Name</b>", defaultValue: app.label, required: false
        }
        section ("<b>For This Device</b>"){

            input(name: "playerDT", type: "capability.musicPlayer", title: "Media Player Device", multiple: false, required:false)      
      	}
        section("<b>Lights</b>") {
			input "dimmers1", "capability.switchLevel", title: "Adjust level of these bulbs", multiple: true, required: false, submitOnChange: true
            input "hues1", "capability.colorControl", title: "Adjust level and color of these bulbs", multiple:true, required:false, submitOnChange: true
            if(hues1||dimmers1) {
            input(name: "iLevelOnPlay1", type: "number", title: "Level on Play", defaultValue:0)
            input(name: "iLevelOnPause1", type: "number", title: "Level on Pause", defaultValue:30)
            input(name: "iLevelOnStop1", type: "number", title: "Level on Stop", defaultValue:100)
            }
            if(hues1) {
				input "colorOnPlay", "enum", title: "Hue Bulbs > Color On Play", required: false, multiple: false, submitOnChange: true,
					options: ["Soft White", "White", "Daylight", "Warm White", "Red", "Green", "Blue", "Yellow", "Orange", "Purple", "Pink"]
                input "colorOnPause", "enum", title: "Hue Bulbs > Color On Pause", required: false, multiple: false, submitOnChange: true,
					options: ["Soft White", "White", "Daylight", "Warm White", "Red", "Green", "Blue", "Yellow", "Orange", "Purple", "Pink"]
                input "colorOnStop", "enum", title: "Hue Bulbs > Color On Stop", required: false, multiple: false, submitOnChange: true,
					options: ["Soft White", "White", "Daylight", "Warm White", "Red", "Green", "Blue", "Yellow", "Orange", "Purple", "Pink"]
                input(name: "tempOnPlay", description: "1000..9999", type: "number", range: "1000..9999", title: "Color Temperature on Play (°K)", required: false)
                input(name: "tempOnPause", description: "1000..9999", type: "number", range: "1000..9999", title: "Color Temperature on Pause (°K)", required: false)
                input(name: "tempOnStop", description: "1000..9999", type: "number", range: "1000..9999", title: "Color Temperature on Stop (°K)", required: false)
            }
            input(name: "bDimOnlyIfOn1", type: "bool", title: "Dim bulbs only if they're already on", required: false)
        }
		section("<b>Switches</b>") {
        	input "switches2", "capability.switch", title:"Switches On when Playing", multiple: true, required: false
            input "switches1", "capability.switch", title:"Switches Off when Playing", multiple: true, required: false
            input(name: "bReturnState1", type: "bool", title: "Switches return to original state when Stopped", required: false)
            input(name: "bSwitchOffOnPause1", type: "bool", title: "Switches use Play config when Paused", required: false)
            input(name: "switchOnPlay", type: "bool", title: "Switches only change on 'Play'", required: false)
            paragraph "The below switches do not toggle off when state becomes inactive, ideal for tiggering external App scenes"
            input "mSwitchPlay", "capability.switch", title:"Momentary switch on Play", multiple: true, required: false
            input "mSwitchPause", "capability.switch", title:"Momentary switch on Pause", multiple: true, required: false
            input "mSwitchStop", "capability.switch", title:"Momentary switch on Stop", multiple: true, required: false
            
        }
		section("<b>Modes</b>") {
			input "playMode1", "mode", title: "Mode when playing", required:false
			input "pauseMode1", "mode", title: "Mode when paused", required:false
			input "stopMode1", "mode", title: "Mode when stopped", required:false
		}
        section("<b>Media Settings</b>") {	
			input(name: "bTreatTrailersAsPause1", type: "bool", title: "Use pause config for movie trailers", required: false)
            input(name: "stopDelay", type: "number", title: "Delay stop action", required:false, defaultValue:0)
            input(name: "pauseDelay", type: "number", title: "Delay pause action", required:false, defaultValue:0)
		}
        section("<b>Restrictions</b>") {
			input "mediaTypeOk", "enum", title: "Only for media types:", multiple: true, submitOnChange: true, required: false,
			options: ['movie', 'episode', 'clip', 'track']
        	input "disabled", "capability.switch", title: "Switch to disable when On", required: false, multiple: false
            input "activeMode", "mode", title: "Only run in selected modes", multiple: true, required:false
        }
    }
}


// Recieve command from MusicPlayer device type
def PlayerDTCommandRecieved(evt){
	def mediaType = playerDT.currentplaybackType ?: null
	if(evt.value=="playing"){		AppCommandRecieved("onplay", mediaType)}
	else if(evt.value=="stopped"){	AppCommandRecieved("onstop", mediaType)}
    else if(evt.value=="paused"){	AppCommandRecieved("onpause", mediaType)}
}



def AppCommandRecieved(command, mediaType) {

// Stop running if disable switch is activated    
    if (disabled != null) {if(disabled.currentSwitch == "on") {logWriter ("Disabled via switch"); return}}
    if (activeMode != null && !activeMode.contains(location.mode)) {logWriter ("Disabled via invalid mode"); return}

// Check if Media Type is correct
	if(mediaTypeOk){
		def mediaTypeFound = mediaTypeOk.find { item -> item == mediaType}
    	if(mediaTypeFound == null) {logWriter ("Match NOT found for media type: ${mediaType}"); return}
	}
    
//Translate play to pause if bTreatTrailersAsPause is enabled for this room
    if(settings?.bTreatTrailersAsPause1 && mediaType == "clip" && command == "onplay") {command = "onpause"}

// Unschedule delays
	unschedule(StopCommand)
    unschedule(PauseCommand)

// Play, Pause or Stop
    if (command == "onplay") {
    	logWriter ("Playing")
        PlayCommand()
    }
    else if (command == "onpause") {        
        logWriter ("Paused")
        if(!settings?.pauseDelay || pauseDelay == "0"){
        	PauseCommand()
        }else{
            logWriter ("Pause Action Delay")
        	runIn(settings?.pauseDelay.value, PauseCommand)
    	}
    }
    else if (command == "onstop") {
        logWriter ("Stopped")
        if(!settings?.stopDelay || stopDelay == "0"){
        	StopCommand()
        }else{
           	logWriter ("Stop Action Delay")
        	runIn(settings?.stopDelay.value, StopCommand)
        }
    }
}

def PlayCommand(){
	if(!state.catcherRunning){
        catchState("switches1")
    	catchState("switches2")
        state.catcherRunning = true
    }
    if(settings?.playMode1){setLocationMode(playMode1)}
	SetLevels(iLevelOnPlay1, colorOnPlay, tempOnPlay)
    SetSwitchesOff()
    mSwitchPlay?.on()
}

def PauseCommand(){
    if(settings?.pauseMode1){setLocationMode(pauseMode1)}
   	SetLevels(iLevelOnPause1, colorOnPause, tempOnPause)
    mSwitchPause?.on()
    if(settings?.bSwitchOffOnPause1) {
   		SetSwitchesOff()
    } else {
       	if(state.catcherRunning && settings?.bReturnState1){
       		returnToState("switches1")
   			returnToState("switches2")
           	state.catcherRunning = false
       	}else{
       		SetSwitchesOn()
           	state.catcherRunning = false
       	}
    }
}

//Stop command
def StopCommand(){

	if(settings?.stopMode1){setLocationMode(settings?.stopMode1)}
    SetLevels(iLevelOnStop1, colorOnStop, tempOnStop)
    mSwitchStop?.on()
    if(state.catcherRunning && settings?.bReturnState1){
       	returnToState("switches1")
    	returnToState("switches2")
        state.catcherRunning = false
    }else{
       	SetSwitchesOn()
        state.catcherRunning = false
    }
}

// Actions
def SetSwitchesOn() {
	if(!switchOnPlay){
		switches1?.on()
    	switches2?.off()
    }
}
def SetSwitchesOff() {
	switches1?.off()
    switches2?.on()
}

def SetLevels(level, acolor, temp) {
	// If color specified set hues
    if (level != null) {
    	def hueColor = 23
		def saturation = 56
		switch(acolor) {
			case "White":
				hueColor = 52
				saturation = 19
				break;
			case "Daylight":
				hueColor = 53
				saturation = 91
				break;
			case "Soft White":
				hueColor = 23
				saturation = 56
				break;
			case "Warm White":
				hueColor = 20
				saturation = 80 //83
				break;
			case "Blue":
				hueColor = 70
				break;
			case "Green":
				hueColor = 35
				break;
			case "Yellow":
				hueColor = 25
				break;
			case "Orange":
				hueColor = 10
				break;
			case "Purple":
				hueColor = 75
				break;
			case "Pink":
				hueColor = 83
				break;
			case "Red":
				hueColor = 100
				break;
		}
        
        if (settings?.bDimOnlyIfOn1){
        	if(acolor != null){ 	hues1?.each 	{ hue -> if ("on" == hue.currentSwitch) 	{ hue.setColor([hue: hueColor, saturation: saturation, level: level]) } } }
            else if(temp != null){ 	hues1?.each 	{ hue -> if ("on" == hue.currentSwitch) 	{ hue.setColorTemperature(temp) } } }
            else {					hues1?.each 	{ hue -> if ("on" == hue.currentSwitch) 	{ hue.setLevel(level) } } }           
        							dimmers1?.each 	{ bulb -> if ("on" == bulb.currentSwitch) 	{ bulb.setLevel(level) } }
        }else{
        	// color takes priority over temperature, dimmers will still set temperature if available
        	if(acolor != null){		hues1?.setColor([hue: hueColor, saturation: saturation, level: level]) }
            else if(temp != null){	hues1?.setColorTemperature(temp) }
			dimmers1?.setLevel(level)
        }
	}
}

//Save state
private catchState(switches) {
        settings."${switches}"?.each { switcher -> state."${switcher.id}State" = switcher.currentValue("switch")
        	logWriter (switcher.currentValue("switch"))
        }
}
//Return to state
private returnToState(switches) {
	settings."${switches}"?.each {switcher -> 
    	if(state."${switcher.id}State" == "on") {switcher.on()}
        if(state."${switcher.id}State" == "off") {switcher.off()}
    }
}


//// GENERIC CODE

private def logWriter(value) {
    if(parent.debugLogging) {log.debug "Media Scene [${app.label}] >> ${value}"}
}
1 Like

@jebbett thank you so much! So do I have to just update the code, or do I have to delete the App and the Room I already created and re-create them from scratch?

Just update the code, no problem

Works perfectly! Thank you so much sir! Do you accept donations of any type? I would be happy to contribute towards your time spent on this.

The offer is appreciated, but just do it mostly for my own learning and to automate my own home, other people have helped me in the past, just spend some time helping someone else :slightly_smiling_face:

1 Like

@jebbett well, your work is appreciated.

Btw, I can use plexJSON.Metadata.title to grab the title of the Movie playing. But I can't figure out how to set a string global variable from within the app itself so I can use it for other reasons, like notifications or reporting to see what my kids watch for example. As an experiment I am using Pushover notifications whenever they play a movie in their room. I can write title to debug. See screenshot.

Do you know the syntax I should use to set a global variable's value to the movie's title and then call it from somewhere else? Do you have any suggestions?

You need to output the value to the driver, you'll need to update the code, it's completely untested , but had a few mins at work:

Add the first line, amend the second to pull the variable from the webhook:

def mediaTitle = plexJSON.Metadata.title
def plexEvent = [:] << [ id: "$playerID", type: "$mediaType", status: "$status", user: "$userName", title: "$mediaTitle" ]

Add to the DTH OUTPUT bit of the code to send to the driver:

pcd.playbackTitle(event.title)

Then update the driver:

Add an attribute, which you can call from other apps:

attribute "playbackTitle", "string"

Add something to handle the request from the app to the driver and update the attribute:

def playbackTitle(title) {
sendEvent(name: "playbackTitle", value: title);
log.debug "Title set to $title"
}

2 Likes

Oh...so an attribute is basically similar to a global variable you can create on the fly? That's pretty cool. Thanks for this. I really appreciate it!

Every device has a number of attributes that can be called, I don't think there are global variables that exist at app level that can be called across apps (although that may have changed), you can get around this to an extent by publishing global events that other apps can pick up, I've used this for cross app communication in the past for other apps I've not published.. one of which allowed me to control my smart home from facebook messenger.. only to see if I could..

One thing to note with the solution I have provided it's likely its not within spec of the "MusicPlayer" device type, so after a quick hunt the device driver should have the attribute "trackDescription", if you update this in the send event 'name' field too then the update will be within spec, and should* work properly if used in the dashboard..

Let me how you get on with my code changes and if it works, if so I'll update the post on here with the new code..

1 Like

Your code works great btw, and the attribute is getting set properly. Thanks for that. But I am still not educated enough on how to pass the value to another rule. I named mine PlexTitle to keep it clear like this.

def playbackTitle(PlexTitle) {
sendEvent(name: "trackDescription", value: PlexTitle);
log.debug "Title set to $PlexTitle"

Logs are showing the correct title but when I use another rule to send notifications, I get either null or $PlexTitle or {PlexTitle}. I'm not sure how else to pass it.

trackDescription is the variable and attribute.. you're calling the wrong thing :slight_smile:

Changing the other variable does nothing..

lol. I'm an idiot.

I do get a null value when I use trackDescription in another rule though.

Not really sure exactly what you are doing and I'm in a hotel currently so can't check on my setup, but I'm guessing you are trying to pull as a global variable, rather than an attribute on a device? You need to specifically pull the attribute from the device you want to monitor, I'm guessing then you need to pass that to a variable to use how you want, but I'm not sure as I've never done what you are trying to do..

So to be clear GlobalVariables in rule machine are not global across hubitat, just across rule machine rules I believe, global within an application not cross application.. if you need to call something from else where you can call the attribute, or set the attribute as a trigger and use that to update a rule machine variable.. Hope that points you in the right direction, but tell them you have an attribute trackDescription and how you want to use it and one of the rule machine experts on here will tell you exactly what you need to do..

1 Like

Yeah, that's exactly what I am trying to do. Pass that attribute value to another rule to use it in a push alert to my phone so I know when my kids start a movie and show the title of the movie.

But your portion of the code works great. You are awesome, and thank you again for your help and time.

@jebbett I forgot to include this screenshot to show you that the attribute of the music device gets updated correctly.

1 Like

@jebbett hey there thanks a lot for this code, but I'm struggling a bit to get things going. I imported all the code and can set up the app fine, along with MediaScene (what a great idea!).

I have an LG TV with Plex app but this doesn't show up in the client list, even if I keep it playing something while I scan. I enabled some debug logging in the code and can see that once I have set it up for a client (my phone), I get some logs when I interact with the TV. The client is correctly identified:

Player JSON: [local:true, publicAddress:xxx.xxx.xxx.xxx, title:LG 65UJ630V-ZA, uuid:6t8vxmclwiolmcxrr1hb0akh] 

However this one never comes up in the list. I checked my PMS and the TV doesn't show up as an authorized device, guessing this is blocking from doing so. I read through the whole thread and there are some indications that it's not mapping things to the right client ID but I am not sure how to fix it. Any help here would be greatly appreciated. Stay safe everyone!

EDIT: Figured I could swap the GUIDs out in the Device properties and sure enough, that did the trick!!