Great thanks, is there anyway for me to track it so I know once it’s complete?
Any update on this?
Thanks!
No ETA on the multpart parsing. But we did add the raw body to the request. Perhaps you can parse what you need that way? if you are on the latest build, you can use "request.body" to get it
Great cheers, I managed to trim the content then extract the required content.. I'm sure there is a more elegant way but it works..
@jpark if you want to have a play then give it a go.. I'm going to be busy the next few weeks moving house.. will post properly once I have some time..
This will only read state from Plex and not control it.. and works using webhooks at the moment and that has been very basic testing.. let me know if it works for you!
App:
import groovy.json.JsonSlurper
/**
* Plex Communicator
*
* Copyright 2018 Jake Tebbett
* Credit To: Christian Hjelseth, iBeech & Ph4r as snippets of code taken and amended from their apps
*
* 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
* ###############
*
* v0.1 - Test Release
*
*/
definition(
name: "Plex Communicator",
namespace: "jebbett",
author: "Jake Tebbett",
description: "Allow SmartThings and Plex to Communicate",
category: "My Apps",
iconUrl: "https://github.com/jebbett/Plex-Communicator/raw/master/icon.png",
iconX2Url: "https://github.com/jebbett/Plex-Communicator/raw/master/icon.png",
iconX3Url: "https://github.com/jebbett/Plex-Communicator/raw/master/icon.png",
oauth: [displayName: "PlexServer", displayLink: ""])
def installed() {
initialize()
}
def updated() {
unsubscribe()
initialize()
}
def initialize() {
// sub to plex now playing response
subscribe(location, null, response, [filterEvents:false])
// Add New Devices
def storedDevices = state.plexClients
settings.devices.each {deviceId ->
try {
def existingDevice = getChildDevice(deviceId)
if(!existingDevice) {
def theHub = location.hubs[0]
log.warn "${deviceId} and ${theHub}"
//def childDevice = addChildDevice("jebbett", "Plex Communicator Device", deviceId, theHub.id, [name: "${deviceId}", isComponent: false])
def childDevice = addChildDevice("jebbett", "Plex Communicator Device", deviceId, theHub.id, [name: deviceId, label: storedDevices."$deviceId".name, completedSetup: false])
}
} catch (e) { log.error "Error creating device: ${e}" }
}
// Clean up child devices
if(settings?.devices) {
getChildDevices().each { if(settings.devices.contains(it.deviceNetworkId)){}else{deleteChildDevice("${it.deviceNetworkId}")} }
}else{
getChildDevices().each { deleteChildDevice("${it.deviceNetworkId}") }
}
// Just in case plexPoller has gasped it's last breath (in case it's used)
if(settings?.stPoller){runEvery3Hours(plexPoller)}
}
preferences {
page name: "mainMenu"
page name: "noAuthPage"
page name: "authPage"
page name: "authPage2"
page name: "clientPage"
page name: "clearClients"
page name: "mainPage"
page name: "ApiSettings"
}
mappings {
path("/statechanged/:command") { action: [GET: "plexExeHandler"] }
path("/p2stset") { action: [GET: "p2stset"] }
path("/pwhset") { action: [GET: "pwhset"] }
path("/pwh") { action: [POST: "plexWebHookHandler"] }
}
/***********************************************************
** Main Pages
************************************************************/
def mainMenu() {
// Get ST Token
try { if (!state.accessToken) {createAccessToken()} }
catch (Exception e) {
log.info "Unable to create access token, OAuth has probably not been enabled in IDE: $e"
return noAuthPage()
}
if (state?.authenticationToken) { return mainPage() }
else { return authPage() }
}
def noAuthPage() {
return dynamicPage(name: "noAuthPage", uninstall: true, install: true) {
section("*Error* You have not enabled OAuth when installing the app code, please enable OAuth")
}
}
def mainPage() {
return dynamicPage(name: "mainPage", uninstall: true, install: true) {
section("Main Menu") {
href "clientPage", title:"Select Your Devices", description: "Select the devices you want to monitor"
href "authPage", title:"Plex Account Details", description: "Update Plex Account Details"
href(name: "ApiSettings", title: "Connection Methods", required: false, page: "ApiSettings", description: "Select your method for connecting to Plex")
}
section("If you want to control lighting scenes then the 'MediaScene' SmartApp is ideal for this purpose"){}
section("This app is developed by jebbett, additional credit goes to Christian H (Plex2SmartThings), iBeech (Plex Home Theatre) & Ph4r (Improved Plex control)."){}
}
}
def ApiSettings() {
dynamicPage(name: "ApiSettings", title: "Select Your Connection Method", install: false, uninstall: false) {
section("1. Plex Webhooks - Plex Pass Only (Best)") {
paragraph("Plex Webhooks is the best method for connecting Plex to SmartThings, however you will need an active Plex Pass Subscription to use it")
href url: "${getLocalApiServerUrl()}/${app.id}/pwhset?access_token=${state.accessToken}", style:"embedded", required:false, title:"Plex Webhooks Settings", description: ""
}
section("2. Plex2SmartThings Program") {
paragraph("This involves running a program on an always on computer")
href url: "${getLocalApiServerUrl()}/${app.id}/p2stset?access_token=${state.accessToken}", style:"embedded", required:false, title:"Plex2SmartThings Program Settings", description: ""
}
section("3. SmartThings Polling *Not Recommended*") {
paragraph("SmartThings will poll every 10 seconds and request the status from Plex, however this method is unreliable and puts increased load on SmartThings and your network (Don't complain to me that it stops working occasionally)")
input "stPoller", "bool", title: "Enable - At your own risk", defaultValue:false, submitOnChange: true
}
if(settings?.stPoller){plexPoller()}
section("NOTE: The settings for both of the above have also been sent to Live Logging, ffor easy access from a computer."){}
log.debug(
"\n ## URL FOR USE IN PLEX WEBHOOKS ##\n${getFullLocalApiServerUrl()}/pwh?access_token=${state.accessToken}"+
"\n\n ## SETTINGS TO USE IN THE EXE ##\n"+
"<!ENTITY accessToken '${state.accessToken}'>\n"+
"<!ENTITY appId '${app.id}'>\n"+
"<!ENTITY ide '${getFullLocalApiServerUrl()}'>\n"+
"<!ENTITY plexStatusUrl 'http://${settings.plexServerIP}:32400/status/sessions?X-Plex-Token=${state.authenticationToken}'>\n")
}
}
def pwhset() {
def html = """<html><head><title>Plex2SmartThings Settings</title></head><body><h1>
${getFullLocalApiServerUrl()}/pwh?access_token=${state.accessToken}<br />
</h1></body></html>"""
render contentType: "text/html", data: html, status: 200
}
def p2stset() {
def html = """
<!DOCTYPE html>
<html><head><title>Plex Webhooks Settings</title></head><body><p>
<!ENTITY accessToken '${state.accessToken}'><br />
<!ENTITY appId '${app.id}'><br />
<!ENTITY ide '${getFullLocalApiServerUrl()}'><br />
<!ENTITY plexStatusUrl 'http://${settings.plexServerIP}:32400/status/sessions?X-Plex-Token=${state.authenticationToken}'>
</p></body></html>"""
render contentType: "text/html", data: html, status: 200
}
/***********************************************************
** Plex Authentication
************************************************************/
def authPage() {
return dynamicPage(name: "authPage", nextPage: authPage2, install: false) {
def hub = location.hubs[0]
section("Plex Login Details") {
input "plexUserName", "text", "title": "Plex Username", multiple: false, required: true
input "plexPassword", "password", "title": "Plex Password", multiple: false, required: true
input "plexServerIP", "text", "title": "Server IP", multiple: false, required: true
}
}
}
def authPage2() {
getAuthenticationToken()
clientPage()
}
def getAuthenticationToken() {
log.debug "Getting authentication token for Plex Server " + settings.plexServerIP
def paramsp = [
uri: "https://plex.tv/users/sign_in.json?user%5Blogin%5D=" + settings.plexUserName + "&user%5Bpassword%5D=" + URLEncoder.encode(settings.plexPassword),
requestContentType: "application/json",
headers: [
'X-Plex-Client-Identifier': 'PlexCommunicator',
'X-Plex-Product': 'Plex Communicator',
'X-Plex-Version': '1.0'
]
]
try {
httpPost(paramsp) { resp ->
state.authenticationToken = resp.data.user.authentication_token;
log.debug "Congratulations Token recieved: " + state.authenticationToken + " & your Plex Pass status is " + resp.data.user.subscription.status }
}
catch (Exception e) { log.warn "Hit Exception $e on $paramsp" }
}
/***********************************************************
** CLIENTS
************************************************************/
def clientPage() {
getClients()
def devs = getClientList()
return dynamicPage(name: "clientPage", title: "NOTE:", nextPage: mainPage, uninstall: false, install: true) {
section("If your device does not appear in the list"){}
section("Devices currently in use by plex will have a [â–º] icon next to them, this can be helpful when multiple devices share the same name, if a device is playing but not shown then press Save above and come back to this screen"){
input "devices", "enum", title: "Select Your Devices", options: devs, multiple: true, required: false, submitOnChange: true
}
if(!devices){
section("*CAUTION*"){
href(name: "clearClients", title:"RESET Devices List", description: "", page: "clearClients", required: false)
}
}else{
section("To Reset Devices List - Untick all devices in the list above, and the option to reset will appear"){}
}
}
}
def clearClients() {
state.plexClients = [:]
mainPage()
}
def getClientList() {
def devList = [:]
state.plexClients.each { id, details -> devList << [ "$id": "${details.name}" ] }
state.playingClients.each { id, details -> devList << [ "$id": "${details.name} [â–º]" ] }
return devList.sort { a, b -> a.value.toLowerCase() <=> b.value.toLowerCase() }
}
def getClients(){
// set lists
def isMap = state.plexClients instanceof Map
if(!isMap){state.plexClients = [:]}
def isMap2 = state.playingClients instanceof Map
if(!isMap2){state.playingClients = [:]}
// Get devices.xml clients
getClientsXML()
// Request server:32400/status/sessions clients - chrome cast for example is not in devices.
executeRequest("/status/sessions", "GET")
}
def executeRequest(Path, method) {
def headers = [:]
headers.put("HOST", "$settings.plexServerIP:32400")
headers.put("X-Plex-Token", state.authenticationToken)
try {
def actualAction = new hubitat.device.HubAction(
method: method,
path: Path,
headers: headers)
sendHubCommand(actualAction)
}
catch (Exception e) {log.debug "Hit Exception $e on $hubAction"}
}
def response(evt) {
// Reponse to hub from now playing request
def msg = parseLanMessage(evt.description);
def stillPlaying = []
if(msg && msg.body && msg.body.startsWith("<?xml")){
log.debug "Parsing status/sessions"
def whatToCallMe = ""
def playingDevices = [:]
def mediaContainer = new XmlSlurper().parseText(msg.body)
mediaContainer.Video.each { thing ->
if(thing.Player.@title.text() != "") {whatToCallMe = "${thing.Player.@title.text()}-${thing.Player.@product.text()}"}
else if(thing.Player.@device.text()!="") {whatToCallMe = "${thing.Player.@device.text()}-${thing.Player.@product.text()}"}
playingDevices << [ (thing.Player.@machineIdentifier.text()): [name: "${whatToCallMe}", id: "${thing.Player.@machineIdentifier.text()}"]]
if(settings?.stPoller){
def plexEvent = [:] << [ id: "${thing.Player.@machineIdentifier.text()}", type: "${thing.@type.text()}", status: "${thing.Player.@state.text()}", user: "${thing.User.@title.text()}" ]
stillPlaying << "${thing.Player.@machineIdentifier.text()}"
eventHandler(plexEvent)
}
}
if(settings?.stPoller){
//stop anything that's no long visible in the playing list but was playing before
state.playingClients.each { id, data ->
if(!stillPlaying.contains("$id")){
def plexEvent2 = [:] << [ id: "${id}", type: "--", status: "stopped", user: "--" ]
eventHandler(plexEvent2)
}
}
}
state.plexClients << playingDevices
state.playingClients = playingDevices
}
}
def getClientsXML() {
//getAuthenticationToken()
log.warn state.authenticationToken
def xmlDevices = [:]
// Get from Devices List
def paramsg = [
uri: "https://plex.tv/devices.xml",
contentType: 'application/xml',
headers: [ 'X-Plex-Token': state.authenticationToken ]
]
httpGet(paramsg) { resp ->
log.debug "Parsing plex.tv/devices.xml"
def devices = resp.data.Device
devices.each { thing ->
// If not these things
if(thing.@name.text()!="Plex Communicator" && !thing.@provides.text().contains("server")){
//Define name based on name unless blank then use device name
def whatToCallMe = "Unknown"
if(thing.@name.text() != "") {whatToCallMe = "${thing.@name.text()}-${thing.@product.text()}"}
else if(thing.@device.text()!="") {whatToCallMe = "${thing.@device.text()}-${thing.@product.text()}"}
xmlDevices << [ (thing.@clientIdentifier.text()): [name: "${whatToCallMe}", id: "${thing.@clientIdentifier.text()}"]]
}
}
}
//Get from status
state.plexClients << xmlDevices
}
/***********************************************************
** INPUT HANDLERS
************************************************************/
def plexExeHandler() {
def status = params.command
def userName = params.user
//def playerName = params.player
//def playerIP = params.ipadd
def mediaType = params.type
def playerID = params.id
//log.debug "$playerID / $status / $userName / $playerName / $playerIP / $mediaType"
def plexEvent = [:] << [ id: "$playerID", type: "$mediaType", status: "$status", user: "$userName" ]
eventHandler(plexEvent)
return
}
def plexWebHookHandler(){
def payloadStart = request.body.indexOf('application/json') + 17
def newBody = request.body.substring(payloadStart)
//log.debug newBody
def jsonSlurper = new JsonSlurper()
def plexJSON = jsonSlurper.parseText(newBody)
//log.debug "Metadata JSON: ${plexJSON.Metadata as String}"
//log.debug "Player JSON: ${plexJSON.Player as String}"
//log.debug "Account JSON: ${plexJSON.Account}"
log.debug "Event JSON: ${plexJSON.event}"
def playerID = plexJSON.Player.uuid
def userName = plexJSON.Account.title
def mediaType = plexJSON.Metadata.type
def status = plexJSON.event
def plexEvent = [:] << [ id: "$playerID", type: "$mediaType", status: "$status", user: "$userName" ]
eventHandler(plexEvent)
}
def plexPoller(){
if(settings?.stPoller){
executeRequest("/status/sessions", "GET")
log.warn "Plex Poller Update"
runOnce( new Date(now() + 10000L), plexPoller)
}
}
/***********************************************************
** DTH OUTPUT
************************************************************/
def eventHandler(event) {
def status = event.status as String
// change command to right format
switch(status) {
case ["media.play","media.resume","media.scrobble","onplay","play"]: status = "playing"; break;
case ["media.pause","onpause","pause"]: status = "paused"; break;
case ["media.stop","onstop","stop"]: status = "stopped"; break;
}
getChildDevices().each { pcd ->
if (event.id == pcd.deviceNetworkId){
pcd.setPlayStatus(status)
pcd.playbackType(event.type)
}
}
}
Driver:
/**
* Plex Communicator Device
*
* Copyright 2018 Jake Tebbett (jebbett)
*
* 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.
*
* 2018-11-17 2.0 Updated to report playbackType Correctly
*/
metadata {
definition (name: "Plex Communicator Device", namespace: "jebbett", author: "jebbett") {
capability "Music Player"
command "playbackType", ["string"]
attribute "playbackType", "string"
}
}
// External
def playbackType(type) {
sendEvent(name: "playbackType", value: type);
log.debug "Playback type set as $type"
}
def setPlayStatus(type){
// Value needs to be playing, paused or stopped
sendEvent(name: "status", value: "$type")
log.debug "Status set to $type"
}
def play() {
sendEvent(name: "status", value: "playing");
}
def pause() {
sendEvent(name: "status", value: "paused");
}
def stop() {
sendEvent(name: "status", value: "stopped");
}
Additionally if you want to control lighting, check out "Media Scene" which can take any player device and control lighting scenes:
Awesome! I'll install it tonight
Just tested and it works great! Exactly what I was looking for!
Thank you!
Setup tips for others:
- Enable OAuth for this app code in HE
- Once initial setup is done, copy the webhooks address from this app to your Plex account Webhooks section.
hmm not working for me
You’re going to need to be a lot more descriptive if you need help? Firstly I assume you have Plex pass and you have configured Plex?
If so what are you getting in habitat logs? What is actually not working? Finding devices, or updating their status?
When I try to control play back nothing happens. I'll get some logs today.
Quoted from my post with the code..
Control is a real PITA as there are 3 or 4 different control methods and each device will usually only support one of them if any at all.. I would have to port over Ph4r's code from his fork of Plex Manager, and there is a lot of code required to do this.. and I don't really have the time to invest in doing this..
Plex Communicator is largely based on PlexPlus which is for subscribing to Plex device states only.. but adds device handling similar to Plex HT Manager, but much better as gets around some of the limitations of the plex API, by discovering some devices that do not appear in the devices list, however the ability to control was not ported.
Personally I didn't see the need for control, which is why I haven't invested the time in to it, I just needed it to control my lighting based on what was playing.. I've also got another app that I need to port which is specifically for the lighting side of things, but this can be achieved via rule machine.. just much easier via my app as it handles a lot of things which are less easy to cater for, such as the small gap between TV episodes can cause issues unless you build in a buffer delay..
Sorry probably not the answer you were looking for, and have no issue if someone wants to do a pull request on the code when I load it to Git to add the functionality.. but unless Ph4r moves to hubitat, then I'm not sure many people will have the time / knowledge to do this easily..
Ok.. well it works half way then. Don't give up ! It's a awesome start !
Haha, I haven't given up, just busy with buying a house, moving and work.. and I have 4 more apps I need to port over at some point that I haven't yet got round to.. just to get my system ported over and off ST..
I honestly wish I had the free time to work on this stuff, the only reason I had time to put in this update was because I was off sick from work with food poisoning
I've got the app installed and working. Curious what everyone is using to in conjunction with it to set things like light levels, etc?
Thanks!
Currently I’m not, (because I’m waiting for Google assistant and I’ve been moving house) however I have a custom written app on ST designed specifically for this purpose... I’ll port over if I have some free time to do so...
Otherwise I expect everyone else is using rule machine..
Thanks!
I looked into using Rule Machine. Looks like it would mostly work, with one exception. In Smartthings I have it setting different lights based on the playback type. Unfortunately I don’t see that capability in Rule Machine as an option.
I’m pretty sure even if it doesn’t exist you can just use a custom event and do it that way.. but not that I’ve used RM since the ST days
I don't see a way to make "playbackType" attribute a rule within RM. I can see "playbackType" being set to either moive/tv depending on the content in the logs but no way to use that info within RM.
Is there a way to turn on/off some virtual switches to indicate content type so we can use it within RM?
Sorry I forgot to reply to this message, if RM doesn’t support then at present no. I would perhaps check in the RM thread though, if it doesn’t exist then it probably should.
Otherwise you have a couple of options, either write an app to handle your use case or wait for me to port mine over.. which I will definitely do, but just moved in to a new house and very busy on that at the moment, will likely be before Christmas though.. or if you feel confident to port it over I can share the ST code with you?