Controlling DirecTV Set Top Box (STB) via Network

From MythTV Official Wiki
Revision as of 02:17, 15 June 2016 by Alarson (talk | contribs) (Update Alternative Script)

(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)
Jump to: navigation, search


Author Lionsnob
Description A Perl script for changing channels on DirectTV STBs using the web interface.
Supports


Summary

Some DirecTV set top boxes now allow certain operations via the DirecTV HTTP Server on port 8080. The H23 is one example. The set top box must be plugged in to the network in order for this to work.

Documentation on the internet is lacking at the moment, but it is possible currently to change the channel and to gather current (tuned) channel information. The attached perl script allows users to change the channel or gather current channel data. The DirecTV set top box always returns data in JSON.

Perl Script

Attached is my first crack at a script. I'm pretty green at perl so this is quite a hack, but it works. The script takes the following arguments:

Always list the IP address of the receiver first.
Next, type the amount of seconds to sleep after changing the channel.
Next, type "tune" to change the channel, or "getTuned" to display the JSON for the current listing
If you chose "tune", now enter the channel number

On Ubuntu, install JSON this way:

$ sudo apt-get install libjson-perl


Application-x-perl.png directtv_http.pl
#!/usr/bin/perl
use LWP::UserAgent;
use JSON;
#use strict;

# Get time for logging, if necessary
#($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst)=localtime(time);
#$year = $year+1900;
#$mon = $mon+1;
$ua = LWP::UserAgent->new;
$ua->agent("$0/0.1 " . $ua->agent);
# $ua->agent("Mozilla/8.0") # pretend we are very capable browser

# If you want to log results of the channel change,
# uncomment the lines that look like:
## print MYFILE ("$year...
## close (MYFILE);
# As well as the lines at the beginning of this script that look like:
# Get time for logging, if necessary
## ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst)=localtime(time);
## $year = $year+1900;
## $mon = $mon+1;
# As well as the file for logging below.  Change the path to something that
# makes sense...
#open (MYFILE, '>>/home/steve/Documents/dtvchangerdata.txt');

# get args
$dtv_ip = $ARGV[0];
$sleep_sec = $ARGV[1];
$arg = $ARGV[2];
$chan_num = "";
$numArgs = $#ARGV + 1;
$tune = 1;
$minor = 0;

if($numArgs == 0)
{
    print "(10) Error: no command line arguments\n";
    exit 1;
}

if($arg eq "getTuned")
{
    $tune = 0;
}
elsif($arg eq "tune")
{
    if(($ARGV[3] =~ m/-/) == 1)
    {
        @chan_arr = split(/-/, $ARGV[3]);
        $chan_num = $chan_arr[0];
        $minor = $chan_arr[1];
    }
    else
    {
        $chan_num = $ARGV[3];
    }
    $tune = 1;
}
else
{
    print "(20) Error: command line argument invalid\n";
    exit 1;
}

$url = 'http://'.$dtv_ip.':8080/tv/tune?major='.$chan_num;
if($tune == 0)
{
    $url = 'http://'.$dtv_ip.':8080/tv/getTuned';
}
if($minor != 0)
{
    $url = 'http://'.$dtv_ip.':8080/tv/tune?major='.$chan_num.'&minor='.$minor;
}
$req = HTTP::Request->new(GET => $url);
$req->header('Accept' => 'text/html');

$i = 0;

while($i < 21)
{
    # send request
    $res = $ua->request($req);

    # check the outcome
    if ($res->is_success) 
    {
        #print $res->content."\n";
        $json = new JSON;
        $json_text = $json->decode($res->content);
        $check_ok = $json_text->{status}->{msg};
        #print "OK? ".$check_ok . "\n";

	if(index($check_ok,"OK") != -1)        
	{
            if($tune == 0)
            {
                print $res->content . "\n";
            }
	    #print "Here 1\n"; 
	    sleep $sleep_sec;
	    #print "Here 2\n";         
	    #print MYFILE ("$year-$mon-$mday $hour:$min:$sec, chan $chan_num: $i failures\n");
            #close (MYFILE);
            exit 0;
        }
        else
        {
            print "(30) Error: " . $check_ok . "\n";
            #print MYFILE ("$year-$mon-$mday $hour:$min:$sec, chan $chan_num: $i failures, (30) Error ".$check_ok."\n");
            #close (MYFILE);
            exit 1;
        }
    }
    $i++; 

    if($i >= 20)
    {
        print "(40) Error: " . $res->status_line . "\n";
        #print MYFILE ("$year-$mon-$mday $hour:$min:$sec, chan $chan_num: $i failures, (40) Error ".$res->status_line."\n");
        #close (MYFILE);
        exit 1;
    }
}
#print MYFILE ("$year-$mon-$mday $hour:$min:$sec, chan $chan_num: $i failures, How did I get here?\n");
#close (MYFILE);
exit 1;


Alternative Script

Using the original directv.pl script as a blueprint, the script below was built to use all available features of the network interface. It has been tested on H20, H21, and HR22 hardware. It should work with additional models but needs further testing.

Basic usage: directv_http.pl ip x.x.x.x command ...

  • Updated on 9/18/12 to correct minor error with the channel up and down keys
  • Updated by James MacLaren on 3/3/15 to allow control of mini genie boxes as well as server (see $clientaddr)
  $clientaddr="XXXXXXXXXXXX"; where XXXXXXXXXXXX is mac address of mini genie without colons. 
  MUST USE CAPS in MAC ADDRESS
  $clientaddr="0" for server
  • Updated by Alarson on 6/14/16.
    • Don't specify URL field "clientAddr" if it is not set in the perl code.
    • Provide some diagnostic messages to help debug mis-configurations of DTV.
    • Minor code cleanup.


Application-x-perl.png directv_http.pl
#!/usr/bin/perl
#
# Copyright (c) 2011 Paul Sands <usg990a at cebridge.net >
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#
# directv_http.pl: Network control of a DirecTV unit via http
# Based upon the directv.pl script maintained by David Gesswein < djg at pdp8online.com >
#  at http://www.pdp8online.com/directv/directv.shtml
# Technical Reference: DTV-MD-0359-DIRECTV SHEF Public Beta Command Set-V1.0.pdf
#
# I take no responsibility for any damage this script might cause.
# Feel free to modify and redistribute this as you see fit, but please retain
# the comments above.
#
# The dtv's "info/getOptions" URL shows the supported query strings,
# including all the supported fields for each.

use Switch;
use LWP::UserAgent;
use JSON;
# use Data::Dumper;  # Only used for debugging.

# Set script version number
$version = "1.1";

# Set client addr
#$clientaddr="XXXXXXXXXXXX";
$clientaddr="";
# Using "/info/getLocations" it would be possible to specify the
# client addresses using the client box names, e.g.,
# clientAddr="BEDROOM".

# The IP address of the dtv.  Default to no ip provided unless explicitly set on command line.
$dtv_ip = "";

# Define a new session and initialize it.
$ua = LWP::UserAgent->new;
$ua->agent("$0/0.1 " . $ua->agent);

# Set default timeout to 2 seconds (stops slow response if DirecTV Receiver is off)
$ua->timeout(2);

# Map commands to function to execute.
# last_param is handled in main routine.
%cmds=("on" => \&on,
      "off" => \&off,
      "reboot" => \&reboot,
      "ip" => \&ip,
      "get_channel" => \&get_channel,
      "get_signal" => \&get_signal,
      "get_systemtime" => \&get_systemtime,
      "get_systemversion" => \&get_systemversion,
      "key" => \&key,
      "serial" => \&serial,
      "delay" => \&delay,
      "version" => \&version,
      "usage" => \&usage,
      );

# Define the keys the script supports
%keymap =
        (power => "power",
       poweron => "poweron",
      poweroff => "poweroff",
        format => "format",
         pause => "pause",
           rew => "rew",
        replay => "replay",
          stop => "stop",
       advance => "advance",
          ffwd => "ffwd",
        record => "record",
          play => "play",
         guide => "guide",
        active => "active",
          list => "list",
          exit => "exit",
          back => "back",
          menu => "menu",
          info => "info",
            up => "up",
          down => "down",
          left => "left",
         right => "right",
        select => "select",
           red => "red",
         green => "green",
        yellow => "yellow",
          blue => "blue",
         ch_up => "chanup",
         ch_dn => "chandown",
          prev => "prev",
             0 => "0",
             1 => "1",
             2 => "2",
             3 => "3",
             4 => "4",
             5 => "5",
             6 => "6",
             7 => "7",
             8 => "8",
             9 => "9",
          dash => "dash",
           "-" => "dash",
         enter => "enter"
        );
# Replace argument last_param with last parameter on the command line.
# The last parameter is then removed.
for ($i = 0; $i < $#ARGV; $i++) {
   if ($ARGV[$i] eq "last_param") {
      $ARGV[$i] = $ARGV[$#ARGV];
      $#ARGV = $#ARGV - 1;
      last;
   }
}

if ($#ARGV < 0) {
   usage();
}

# Loop through each arguement passed on the commandline
while ($#ARGV >= 0) {
   if (defined($sub = $cmds{$ARGV[0]})) {
      shift @ARGV;
      &$sub;
   } else {
      if ($ARGV[0] == 0) {
         usage();
         die "\nCommand $ARGV[0] not found\n"
      }
      change_channel($ARGV[0]);
      shift @ARGV;
   }
}

exit 0;

# Give the user the syntax for using the script
sub usage {
   print "Usage: $0 ip x.x.x.x command ...\n";
   print "Commands:\n";
   print "  version         - display program version\n";
   print "  ip x.x.x.x      - IP address of box - REQUIRED\n";
   print "                     must be the first command\n";
   print "  on              - turn box on\n";
   print "  off             - turn box off\n";
   print "  reboot          - hard reboot (reset) - WARNING Requires MANUAL reactivation of network control\n";
   print "  delay number    - wait for number seconds. Floating point is valid \n";
   print "  number{-number} - change to specified channel-subchannel\n";
   print "                     must be the last command\n";
   print "\n";
   print "  get_channel     - print current channel\n";
   print "  get_systemtime  - print date and time (add epoch to return in epoch format)\n";
   print "  get_signal      - print signal strength\n";
   print "  key {key}       - send key to receiver\n";
   print "                           enter "; print chr(34); print"key help"; print chr(34); print " for list of available keys\n";
   print "  serial          - process hex coded serial command (hex only; no need for 0x)\n";
   print "\n";
}

# Provide help text for the keys the script supports
sub keyhelp {
   print "Usage: $0 ip x.x.x.x key {key}\n";
   print "\n";
   print "                             Valid Keys:\n";
   print "Receiver Power             - power, poweron, poweroff\n";
   print "Change Output Format       - format\n";
   print "Live TV / DVR Playback     - pause, rew, replay, stop, advance, ffwd, record, play\n";
   print "Interactive TV             - guide, active, list\n";
   print "Navigation                 - up, down, left, right, back, menu, exit, info, select\n";
   print "Favorites                  - red, green, yellow, blue\n";
   print "Channel                    - chanup, chandown, prev\n";
   print "Numbers                    - 0-9, dash (or -), enter\n";
   print "\n";
}

sub on {
    require_ip();
    my $url = 'http://'.$dtv_ip.':8080/remote/processKey?key=poweron'.client_addr('&');
    $status = send_req($url);
    #my $key_rcv = $json_text->{key} if ($status == 0);
    #print "$key_rcv\n" if ($status == 0);
    exit $status
}

sub off {
    require_ip();
    my $url = 'http://'.$dtv_ip.':8080/remote/processKey?key=poweroff'.client_addr('&');
    $status = send_req($url);
    #my $key_rcv = $json_text->{key} if ($status == 0);
    #print "$key_rcv\n" if ($status == 0);
    exit $status
}

sub reboot {
    require_ip();
    my $url = 'http://'.$dtv_ip.':8080/serial/processCommand?cmd=0xf7'.client_addr('&');
    $status = send_req($url);
    # Command executes with no response back to the script
    exit $status
}

sub ip {
   $dtv_ip = $ARGV[0];
   shift @ARGV;
}

sub get_channel {
   require_ip();
   my $url = 'http://'.$dtv_ip.':8080/tv/getTuned'.client_addr('?');
   $status = send_req($url);
   my $chan = $json_text->{major} if ($status == 0);
   if ($status == 0) {
       print "$chan\n";
   } else {
       print "When access is denied, on your DTV try: Menu->System Setup->Whole-Home->External Device, set 'Current Program' to 'Allow'.\n";
   }
   exit $status
}

sub serial {
   require_ip();
   my $url = 'http://'.$dtv_ip.':8080/serial/processCommand?cmd=FA'.$ARGV[0].client_addr('&');
   $status = send_req($url);
   my $ser = $json_text->{return}->{data} if ($status == 0);
   print "$ser\n" if ($status == 0);
   print "$url\n" if ($status == 1);
   exit $status
}

sub get_signal {
   require_ip();
   my $url = 'http://'.$dtv_ip.':8080/serial/processCommand?cmd=FA90'.client_addr('&');
   $status = send_req($url);
   my $signal = $json_text->{return}->{data}  if ($status == 0); $signal = hex($signal)  if ($status == 0);
   print "$signal\n" if ($status == 0);
   exit $status
}

sub get_systemtime() {
   require_ip();
   # Assume we don't want to return time from epoch unless given commandline override
   my $epoch = $ARGV[0] eq "epoch";
   # According to "/info/getOptions" on the H24, clientAddr is not a legal field for "getVersion".
   my $url = 'http://'.$dtv_ip.':8080/info/getVersion'; #.client_addr('?');
   $status = send_req($url);
   my $systime = $json_text->{systemTime}; if (!$epoch == 1 ) { $systime = scalar localtime($systime)};
   print "$systime\n" if ($status == 0);
   exit $status
}

sub get_systemversion() {
   require_ip();
   # According to "/info/getOptions" on the H24, clientAddr is not a legal field for "getVersion".
   my $url = 'http://'.$dtv_ip.':8080/info/getVersion'; #.client_addr('?');
   $status = send_req($url);
   my $sysversion = $json_text->{version} if ($status == 0);
   print "$sysversion\n" if ($status == 0);
   exit $status
}

sub key () {
   #if (shift(@ARGV) eq "help") {
   if (@ARGV =~ /help/) {
      keyhelp();
   } else {
   send_key(shift(@ARGV));
   }
}

sub delay {
   select(undef, undef, undef, $ARGV[0]);
   shift @ARGV;
}

sub version {
   print "Script Version $version\n";
}

sub change_channel () {
   require_ip();
   my ($chan_major,$chan_sub)= split /-/,@_[0];
   my $url = 'http://'.$dtv_ip.':8080/tv/tune?major='.$chan_major.client_addr('&');
   $url = $url . '&minor='.$chan_sub if ($chan_sub ne "");
   $status = send_req($url);
   if ($status != 0) {
       print "When access is denied, on your DTV try: Menu->System Setup->Whole-Home->External Device, set 'External Access' to 'Allow'.\n";
   }
   exit $status
}

sub send_key () {
   require_ip();
   my $url;
   my $ky;
   my @keys = split / /,@_[0];
   foreach $ky (@keys)
   {
      my $key = $keymap{$ky};
      die "Unknown key $ky\n" if (!defined($key));
      $url = 'http://'.$dtv_ip.':8080/remote/processKey?key='.$key.client_addr('&');
      $status = send_req($url);
   }
}

sub send_req () {
      my ($url) = @_;
#      print $url."\n";
      my $parent = ( caller(1) )[3];
      $parent =~ s/main:://g;
      $req = HTTP::Request->new(GET => $url);
      $req->header('Accept' => 'text/html');
      $res = $ua->request($req);
#      print "\n".Dumper($res);
      #sleep $delay_sec if ($delay > 0);
      if ($res->is_success)
      {
         $json = new JSON;
         $json_text = $json->decode($res->content);
         $check_ok = $json_text->{status}->{msg};
         if(($check_ok eq "OK.") || ($check_ok eq "OK"))
         {
            return 0;
            #}
         } else {
            print "Failed!\n";
            return 1;
         }
      } else {
         print "Failed!\n";
        return 1;
      }
}

# Return a string to specify the clientAddr URL field.
# seperator should be either "?" if clientAddr is the first URL field, otherwise "&".
sub client_addr() {
    my ($seperator) = @_;

    # If clientaddr is blank, then don't include the field at all.
    return "" if ($clientaddr == "");
    return $seperator . "clientAddr=" . $clientaddr;
}

# Exit with an error if the IP address has not been specified.
sub require_ip() {
    return if $dtv_ip ne "";
    usage();
    print "No IP Address given\n";
    exit 1;
}

Documentation

I've found some official looking documentation in PDF form Here: http://www.sbcatest.com/TechUpdates/DTV-MD-0359-DIRECTV%20SHEF%20Public%20Beta%20Command%20Set-V1.0.pdf It includes commands and responses for DVR and TV functionality, and even a way to emulate remote control button presses.