WOL Plugin

Hi there,

Sorry if I am posting this in the wrong place on the forum.

I’m not really experienced at Python or Plex channel development but I’m trying to get something put together, and could use some help please.

The idea: To create a plex channel that can be used to make the PMS send a Wake on LAN (WOL) request to another device on the local network.

For example, I have a PMS which I run 24x7. I also have some HTPC, which I don’t leave on the time. I might want to access recordings or DVR functionality on those HTPC. The idea is to use a Plex plugin to wake those devices so that I can access the content.

I’ve drafted some code below, by borrowing bits from various places. Ideally I’d want the plug configurable so that I can have 3 different HTPC devices that could be woken up.

What each device will need:
-A name, or alias
-Its MAC Address
-Port Number for sending the request (usually 7 or 9)
-An optional broadcast or forwarding address (255.255.255.255 for a local subnet broadcast).

Code below. This is just proof of concept code but it doesn’t give a channel icon. Any help appreciated.

Many thanks,

Andrew

ART = ‘artlogo.jpg’
ICON = ‘icon-default.jpg’

NAME = ‘WOL Utility’

TARGET_ONE_MAC = ‘60a44cd03a99’
TARGET_ONE_ALIAS = ‘LR-PC’
TARGET_ONE_PORT = ‘9’
TARGET_ONE_BROADCAST = ‘255.255.255.255’

####################################################################################################
def Start():

ObjectContainer.art = R(ART)
ObjectContainer.title1 = NAME
TrackObject.thumb = R(ICON)

####################################################################################################
@handler(’/Utility/WOL’, NAME, thumb=ICON, art=ART)
def MainMenu():

sendmagic(macaddress=TARGET_ONE_MAC, alias=TARGET_ONE_ALIAS, DEFAULT_PORT=TARGET_ONE_PORT, BROADCAST_IP=TARGET_ONE_BROADCAST)

return 

####################################################################################################
def sendmagic(macaddress, alias, DEFAULT_PORT, BROADCAST_IP):

import socket
import struct

if len(macaddress) == 12:
    pass
elif len(macaddress) == 17:
    sep = macaddress[2]
    macaddress = macaddress.replace(sep, '')
else:
    raise ValueError('Incorrect MAC address format')

# Pad the synchronization stream
data = b'FFFFFFFFFFFF' + (macaddress * 20).encode()
send_data = b''

# Split up the hex values in pack
for i in range(0, len(data), 2):
    send_data += struct.pack(b'B', int(data[i: i + 2], 16))



packets = []
ip = kwargs.pop('ip_address', BROADCAST_IP)
port = kwargs.pop('port', DEFAULT_PORT)
for k in kwargs:
    raise TypeError('send_magic_packet() got an unexpected keyword '
                    'argument {!r}'.format(k))

for mac in macs:
    packet = create_magic_packet(mac)
    packets.append(packet)

sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
sock.connect((ip, port))
for packet in packets:
    sock.send(packet)
sock.close()

return

Any ideas please dudes?

The latest version nearly works :slight_smile: It doesn’t wake the PC, and I can see in the PMS logs that its not too happy with the plugin.

Latest code:
ART = ‘artlogo.jpg’
ICON = ‘icon-default.jpg’

NAME = ‘WOL Utility’

TARGET_ONE_MAC = ‘60a44cd03a99’
TARGET_ONE_ALIAS = ‘LR-PC’
TARGET_ONE_PORT = 9
TARGET_ONE_BROADCAST = ‘255.255.255.255’

TARGET_TWO_MAC = ‘60a44cd03a99’
TARGET_TWO_ALIAS = ‘LR-PC-7’
TARGET_TWO_PORT = 7
TARGET_TWO_BROADCAST = ‘255.255.255.255’

TARGET_THREE_MAC = ‘60a44cd03a99’
TARGET_THREE_ALIAS = ‘LR-PC’
TARGET_THREE_PORT = 9
TARGET_THREE_BROADCAST = ‘255.255.255.255’

####################################################################################################
def Start():

ObjectContainer.art = R(ART)
ObjectContainer.title1 = NAME
TrackObject.thumb = R(ICON)

####################################################################################################
@handler(’/Utility/WOL’, NAME, thumb=ICON, art=ART)
def MainMenu():

oc = ObjectContainer()
oc.add(CreateTrackObject(macaddress=TARGET_ONE_MAC, alias=TARGET_ONE_ALIAS, DEFAULT_PORT=TARGET_ONE_PORT, BROADCAST_IP=TARGET_ONE_BROADCAST))

oc.add(CreateTrackObject(macaddress=TARGET_TWO_MAC, alias=TARGET_TWO_ALIAS, DEFAULT_PORT=TARGET_TWO_PORT, BROADCAST_IP=TARGET_TWO_BROADCAST))

oc.add(CreateTrackObject(macaddress=TARGET_THREE_MAC, alias=TARGET_THREE_ALIAS, DEFAULT_PORT=TARGET_THREE_PORT, BROADCAST_IP=TARGET_THREE_BROADCAST))


return oc

####################################################################################################
def CreateTrackObject(macaddress, alias, DEFAULT_PORT, BROADCAST_IP, include_container=False):

track_object = DirectoryObject(key=Callback(sendmagic, macaddress=macaddress, alias=alias, DEFAULT_PORT=DEFAULT_PORT, BROADCAST_IP=BROADCAST_IP ),title=alias)

if include_container:
	return ObjectContainer(objects=[track_object])
else:
	return track_object

####################################################################################################
def sendmagic(macaddress, alias, DEFAULT_PORT, BROADCAST_IP):

import socket
import struct

if len(macaddress) == 12:
	pass
elif len(macaddress) == 17:
	sep = macaddress[2]
	macaddress = macaddress.replace(sep, '')
else:
	raise ValueError('Incorrect MAC address format')

send_data = []

# Pad the synchronization stream
data = b'FFFFFFFFFFFF' + (macaddress * 20).encode()
send_data = b''

# Split up the hex values in pack
for i in range(0, len(data), 2):
	send_data += struct.pack(b'B', int(data[i: i + 2], 16))

sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
sock.connect((BROADCAST_IP, DEFAULT_PORT))
sock.send(send_data)
sock.close()

return

Files attached :slight_smile: Any help appreciated :smiley:

you probably want to structure it more like this:

import socket
import struct

PREFIX = '/Utility/WOL'

@handler(PREFIX, NAME, thumb=ICON, art=ART)
def MainMenu():
    ...

@route(PREFIX + '/sendmagic', DEFAULT_PORT=int)
def sendmagic(macaddress, alias, DEFAULT_PORT, BROADCAST_IP):
    Log.Info('sendmagic: {}, {}, {}, {}'.format(macaddress, alias, DEFAULT_PORT, BROADCAST_IP))
    if len(macaddress) == 12:
        pass
    elif len(macaddress) == 17:
        sep = macaddress[2]
        macaddress = macaddress.replace(sep, '')
    else:
        raise ValueError('Incorrect MAC address format')

    send_data = []

    # Pad the synchronization stream
    data = b'FFFFFFFFFFFF' + (macaddress * 20).encode()
    send_data = b''

    # Split up the hex values in pack
    for i in range(0, len(data), 2):
        send_data += struct.pack(b'B', int(data[i: i + 2], 16))

    sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
    sock.connect((BROADCAST_IP, DEFAULT_PORT))
    sock.send(send_data)
    sock.close()

    return ObjectContainer()

I added a Log line in there so you can see if the plex routing is passing the values correctly in the log file.

Many thanks coryo123. I’ll give what you suggested a try :slight_smile:

At the moment I kind-of got my version working - it is waking machines, but after completing the WOL action Plex says that the channel is not responding - is that because I’m not returning an ObjectContainer() ? After performing the action I just want it to go back to the main Plex menu / Channel List.

Thanks again :smiley:

yeah the plex client expects a container to return from a route function.

you can also try returning:

return MessageContainer(header='success', message='success')

different clients handle it differently though, ideally it would just pop up a message and the client would remain at the menu it was at.

Hi there,

Thanks for your help so far. The plug-in is nearly finished :slight_smile:

It wakes systems, and can be configured for up to 10 different systems to wake.

The only problem I have is that the Plugin doesn’t appear on the Android app. It appears in Chrome on Android, but not on the native App.

How do I diagnose this please, or do you know what the problem might be?

Latest version attached :smiley:

Thanks again for your help.

Most likely an issue with your prefix.
Change your prefix to:

PREFIX = '/applications/wol'

within your __init__.py file.

Also make sure to remove the empty lines from your DefaultPrefs.json file. Probably not hurting anything, but just to be safe and good practice.

Edit: forgot to mention, you will most likely need to restart your PMS to see changes take effect.

Thanks Twoure and coryo123 :slight_smile:

Nearly there, those last few tweaks sorted it.

I have only one thing left which isn’t quite right. When the WOL packet has been sent (using the function sendmagic) it displays a message, which is what I wanted. Plex then takes the user back to an empty WOL Utility object container - if you then click back you then get the regular object container, containing the list of servers available.

I think its something wrong with how I’ve finishing or returning from sendmagic, can you help please? :slight_smile:

Latest version attached.

Thanks again :smiley:

@SpacemanJT said:
I have only one thing left which isn’t quite right. When the WOL packet has been sent (using the function sendmagic) it displays a message, which is what I wanted. Plex then takes the user back to an empty WOL Utility object container - if you then click back you then get the regular object container, containing the list of servers available.

I think its something wrong with how I’ve finishing or returning from sendmagic, can you help please? :slight_smile:

That is normal behavior for the MessageContainer function. If you want your sendmagic function to return a message and send the user back to the main menu, then you can use the following:

Instead of having sendmagic return a MessageContainer, redirect it back to the main menu and set the main menu header and message. This will provide feedback and send the user back to the main menu.
Change the return in sendmagic to:

return MainMenu(header='success', message='Request set to ' + alias)

Then in the MainMenu function change to the following:

def MainMenu(header=None, message=None):
    oc = ObjectContainer(header=header, message=message)
    #... the rest is the same

Setting the variables to None within the function will keep the popup message from displaying. Also it sets the defaults for those values.

Some caveats:

  1. header & message values in the ObjectContainer and MessageContainer will return errors for the Plex Home Theater (PHT) and OpenPHT clients.
  2. Channel history may get convoluted

For (1) you can add an intermediate function in-between sendmagic and MainMenu to check for client platform or product (info here). So if the client is PHT or OpenPHT, then do not return a message, or you can do something similar to my messages.py to still provide some form of feedback for those clients.

For (2) you will have to play with no_history and replace_parent within the sendmagic object container.

Some finishing Notes:

  • Normally all caps variables (aka ICON) are global variables when defined at the top of your file. I would suggest changing some of your functions variables to lowercase to avoid any future confusion, but is not necessary.
  • Currently your sendmagic function has no case to handle errors or provide feedback to the user that an error occurred. Once you get things working, I suggest adding some user feedback for when errors occur. Once again, not necessary.

Thanks Twoure, I’ll try your suggestions. I was going to do some more preferences validity checking and reporting, and yes, I will tidy up the names. This is my first proper channel or plugin, and I know I’ve re-used a lot of code, but hopefully I’ll get there :slight_smile:

Is this kind of plugin worth posting on GitHub? How do I offer to get it listed on the Unofficial Appstore, or isn’t it worth it?

Thanks again for all of your help.

P.S. How do I set no_history within the sendmagic object container?

@SpacemanJT said:
Is this kind of plugin worth posting on GitHub? How do I offer to get it listed on the Unofficial Appstore, or isn’t it worth it?

To be honest, I didn’t actually try your channel, but seems like it could be useful (I’ll give it a try later, just don’t have time right now). GitHub is a good way to keep track of changes and allow others to easily help edit your channel, so yes I’d push it to GitHub. As for adding to the Unsupported App Store v2 (UAS2), follow the guide here, and example pull request for clarity.

P.S. How do I set no_history within the sendmagic object container?

no_history, replace_parent, and no_cache are all bool so for example:

ObjectContainer(no_history=True, replace_parent=True, no_cache=True)

so the above will have no history within the cache, and since no_cache is True the Container will be called each time fresh instead of using a cached response. replace_parent is supposed to go two steps back in the channel hierarchy, i.e. A → B → C, (all are ObjectContainers) if C has replace_parent set to True then when a user hits the back button it should take them to A.

Edit: Just realized you didn’t have an ObjectContainer within the sendmagic function, so don’t need the no_history stuff. Sorry.

Hi there,

I’ve attached the latest version, if you want to try it later :slight_smile:

Thanks.

You may want to include the Prefs container in the MainMenu so the user can change the settings from within the channel.

To do so add the following to MainMenu above the return oc line:

oc.add(PrefsObject(title='Preferences'))

The Prefs object is not available on all clients, but there is a current work around outlined here, (thread here). Example usage here.

Thanks for your help. I decided not to add more Preferences objects, since this plugin would normally be configured just once, and then left.

I’ve completed the error checking, and also implemented a Wake-On-LAN Group, to allow multiple systems to be woken at the same time.

I’m sure this isn’t the best Python you’ve ever seen :slight_smile:

Thanks for an ingenious solution, SpacemanJT! It was just the tool I was looking for!

Happy Easter!

Best regards,

Vegard

You are welcome, glad its useful :slight_smile: