Jump to content

I created a Perl script to provide an ISY-26 commandline too


rubin

Recommended Posts

Posted

I spent a couple hours tonight hacking up a perl script to communicate with the ISY-26 web interface, since there is no API.

 

The result is you can turn devices or scenes on and off from the command line. Thought I'd post it here since I couldn't find anything like it for the -26.

 

I'll try to inline it here, but for a clean copy try http://simplanet.org/~rubin/isy.pl

 

Tested on ubuntu linux.

 

Here are some examples:

 

Provide a list of devices and their node names (needed for other commands)

isy.pl -host 192.168.1.10 -query -password mypass

 

Turn on a light:

isy.pl -host 192.168.1.10 -on -node "5 48 76 1" -password mypass

 

View node names for your scenes

isy.pl -host 192.168.1.10 -query -password mypass -type scenes

 

execute a "fast off" for a scene

isy.pl -host 192.168.1.10 -fastoff -password mypass -type scenes -node 28328

 

[see reply below for improved version]

Posted

Rubin,

 

This is very cool! Thanks for contributing the very useful script.

 

It also runs fine on Mac OS X Snow Leopard (however, the pQuery module, which is not not part of the OS X Perl distribution, also needs to be installed).

 

One minor bug: the script won't work out of the box if the ISY username has been changed from the default of "admin" because there is no option to specify username on the command line. This is easily fixed by editing the #Defaults section of the script.

 

Cheers,

-Jim

Posted
Rubin,

 

however, the pQuery module, which is not not part of the OS X Perl distribution, also needs to be installed).

 

I actually didn't end up using pQuery, so you can just delete the "use pQuery" line. I left it in by mistake.

 

Here is a new version with that fixed, and the ability to query just a specific node for status (on/off) useful for building other scripts to toggle status.

 

#!/usr/bin/perl -w
#
# Created by Rubin


=head1 NAME

isy.pl - Control an isy-26 via its web interface, from commandline

=head1 SYNOPSIS

isy.pl  [options]

   Commands: (short)
   -query    (-q)
   -on       
   -off      
   -faston
   -fastoff

   Options:  (short)
   -help     
   -verbose  (-v)
   -host
   -cfile
   -username
   -password
   -protocol (-p)
   -type     (-t)
   -node     (-n)


=head1 OPTIONS

=over 8

=item B

Fetches the list of devices or scenes from the ISY and prints them. If you specify a -node, only that node details will be printed.
It will print the status by itself by default, but if called with -verbose, name, node, and status will be printed.

=item B

Action: Turn something on

=item B

Action: Turn something off

=item B

Action: Turn something fast-on

=item B

Action: Turn something fast-off

=item B

Action: Dim something by one level

=item B<-brighten

Action: Brighten something by one level

=item B

Show this message

=item B

Prints more details while running.

=item B

Specifies if you are interacting with Devices or Scenes. Default is Devices

=item B

Which node to act on for actions

=item B

A credencial file to read the password from. The password must be on the 1st line of the file. 

=item B

Username to use. Defaults to admin

=item B

Password to use. Passing passwords in arguments is extremely insecure in *NIX type operating systems, as they show up in the process list. Use -cfile instead of this.

=back

=head1 DESCRIPTION

A tool to talk to an isy-26 via its html/form interface from commandline

=cut

use strict;

use Data::Dumper;
use Carp qw(confess cluck);
use File::Slurp;
use Getopt::Long;
use Pod::Usage;

#use FindBin qw($RealBin);
#use lib "$RealBin";

use LWP::UserAgent;
use HTTP::Request::Common qw(POST GET);
use HTML::TreeBuilder;

#Defaults
my $default_type = "devices";
my $username = "admin";
my $protocol = "http";
my $host = "isy";
my $password = "";

my %options;
GetOptions( \%options, 'verbose', 'help', 'type:s', 'query', 'on', 'off', 'faston', 'fastoff', 'dim', 'brighten', 'node:s', 'repeat:i', 'repeatdelay:i', 'host:s', 'password:s', 'username:s', 'cfile:s' ) or confess("Error");

if ($options{'help'}) {
   pod2usage(0); 
   exit 0;
}

if($options{'verbose'}) {
   print "Being verbose\n";
}

if($options{'host'}) {
   $host = $options{'host'};
}
else {
   print "No host specified. You must use -host.\n";
   pod2usage(1);
   exit 1;
}

if($options{'cfile'}) {
   # open cfile and read password from it
   if( -f $options{'cfile'}) {
       my @content = read_file($options{'cfile'});
       $password = $content[0];
       $password =~ s/\r|\n//g;
   }
}
else {
   if($options{'password'}) {
       $host = $options{'password'};
   }
   else {
       print "No password specified. You must use -password or -cfile\n";
       exit 1;
   }
}

if($options{'username'}) {
   $username = $options{'username'};
}

if($options{'protocol'}) {
   if($options{'protocol'} =~ /^http[s]?/) {
       $protocol = $options{'protocol'};
   }
   else {
       print "Unsupported protocol. http or https only\n";
       exit 1;
   }
}

# Setup the url now that we know the host and user/pass info
my $url = "$protocol://$username:$password\@$host";

# Called by query() to display the results of the query
sub parse_result {
  my $content = shift;
  my $page = HTML::TreeBuilder->new();
  $page->parse($content);
  $page->eof;

  #Find any forms that post to "/change" and iterate over them
  #TODO: Should adjust this to itterate over  first so we can snipe the name and status too

  my %items; #items that can be adjusted on the page, ie, devices or scenes
  foreach my $row ($page->look_down( '_tag', 'tr')) {

     # For each form, iterate over its inputs building a hash
     my %item; 
     my $colcounter = 0;
     foreach my $td ($row->look_down( '_tag', 'td')) {
       $colcounter++;
       if($colcounter == 1) {
           #print "DEBUG: Got col 1, setting Description\n";
           $item{'name'} = $td->as_text;
       }
       elsif($colcounter == 2) { #This will only be status on DEVICES page
           my $td_content = $td->as_text;
           if($td_content =~ /^on|off$/i) {
               #print "DEBUG: Got col 2, setting status\n";
               $item{'status'} = $td_content;
           }
       }

     }
     foreach my $form ($row->look_down( '_tag', 'form', sub { $_[0]->{'action'} eq "/change" } )) {
         #print "DEBUG: Got a form ". $form->{'method'}." to ". $form->{'action'}. "\n";
         foreach my $input ($form->look_down( '_tag', 'input', sub { $_[0]->{'type'} ne "submit"} )) {
           #print "DEBUG: Got an input: ". $input->{'value'}. "\n";
           $item{$input->{'name'}} = $input->{'value'};
         }
         #print "DEBUG: Built an item:\n";
         #print Dumper(\%item);
         #print "\n";
     }
     if($item{'node'}) {
         $items{$item{'node'}} = \%item;
     }
     #else {
     #  print "DEBUG: not adding this row because no node was found\n";
     #}
  }
  #print "DEBUG: Built an item tree\n";
  #print Dumper(\%items);
  #print "\n\n";
  return \%items;
}

sub print_result {
   my $items = shift;
   my $type = shift;

   print "Available $type:\n";
   printf("---------------------------------------------------------\n");
   printf("%30s:   %13s  %6s\n", "Name", "Node", "Status");
   printf("---------------------------------------------------------\n");
   foreach my $node (keys %$items) {
       my %item = %{$items->{$node}};
       #print "DEBUG: Got an item: $node\n";
       #print Dumper(\%item);
       printf("%30s: ", $item{'name'});
       printf("  %13s", $item{'node'});
       if($item{'status'}) {
           printf("  %6s", $item{'status'});
       }
       print "\n";
   }
   #print "DEBUG: printing results\n";
   #print Dumper($items);
}

sub parse_single_result {
  my $content = shift;
  my $node = shift;
  my $page = HTML::TreeBuilder->new();
  my $name = undef;
  $page->parse($content);
  $page->eof;

  #Find 2nd  and then 1st  is status on/off
  my $trcount = 0;
  my $status = undef;
  foreach my $row ($page->look_down( '_tag', 'tr')) {
       $trcount++;
       if($trcount == 2) {
           my $tdcount = 0;
           foreach my $tdrow ($row->look_down( '_tag', "td")) {
               $tdcount++;
               if($tdcount == 1) {
                   $status = $tdrow->as_text;
               }
           }
       }
  }
  #Name is first h2 block contents
  foreach my $h2 ($page->look_down( '_tag', 'h2')) {
       $name = $h2->as_text;
       last;
  }
  if(defined $status) {
       return({name=>$name, node=>$node, status=>$status});
  }
  return undef;
}
sub print_single_result {
   my $result = shift;
   if($options{'verbose'}) {
       print $result->{'name'}.":\n";
       print "  node: ". $result->{'node'}. "\n";
       print "  status: ". $result->{'status'}. "\n";
   }
   else {
       print $result->{'status'} . "\n";
   }
}

sub query {
   my $url = shift;
   my $type = shift;
   my $node = shift;

   #Create a LWP instance and request
   my $ua = LWP::UserAgent->new;
   my $req = HTTP::Request->new(GET => $url);
   my $res = $ua->request($req);

   if($res->is_success) {
       if($options{'verbose'}) {
           print "Got Result: ";
           #print $res->content;
       }
       if($node) {
           print_single_result(parse_single_result($res->content, $node));
       }
       else { # list all nodes
           print_result(parse_result($res->content), $type);
       }
   }
   else {
       print "Query to $host returned an error code:";
       print $res->status_line, "\n";
       exit 1;
   }
}

# Send a POST to the isy to turn something on/off/etc
sub action {
   my $url = shift;
   my $action = shift;
   my $type = shift;
   my $node = shift;
   $node =~ s/ /+/g;

   my $repeat = 1;
   my $repeatdelay = 0;
   if($options{'repeat'} && $options{'repeat'} > 1 && $options{'repeat'} <10> 1 && $options{'repeatdelay'} < 100) {
       $repeatdelay = 0 + $options{'repeatdelay'};
   }
   if($node) {
       for(my $i = 1; $i new;
           my $req = HTTP::Request->new(POST => $url."/change");
           $req->content_type('application/x-www-form-urlencoded');
           my $content = "node=$node&raddress=$type&submit=$action";
           $req->content($content);
           #print "DEBUG: content is: $content\n";
           my $res = $ua->request($req);

           if($res->is_success) {
               if($options{'verbose'}) {
                   print "Action posted successfully\n";
                   #print $res->content;
               }
               #$res->content;
           }
           else {
               print "Action returned an error code:";
               print $res->status_line, "\n";
               print "Content was:\n$content\n";
               exit 1;
           }
           if($i < $repeat) {
              sleep($repeatdelay);
           }
       }
   }
   else {
       print "You must specify a node with -node for actions\n";
       pod2usage(1);
       exit 1;
   }
}

# If they pass a type, validate it. 
my $type = $default_type;
if($options{'type'}) {
   if($options{'type'} =~ /^devices|scenes$/) {
     $type = $options{'type'};
   }
   else {
       print "Unknown type: ". $options{'type'}. ".  ";
       print "Try one of: devices | scenes\n";
       exit 1;
   }
}

# Main command list
if($options{'query'}) {
   if($options{'verbose'}) {
       print "Querying type $type\n";
   }
   if($options{'node'}) {
       if($type ne "devices") {
           print "Query of a specific node only available with type devices\n";
           exit 1;
       }
       query("$url/settings?node=".$options{'node'}, $type, $options{'node'});
   }
   else {
       query("$url/$type/", $type);
   }
   exit 0;
}
elsif($options{'on'}) {
  action("$url", "On", $type, $options{'node'});
  exit 0;
}
elsif($options{'off'}) {
  action("$url", "Off", $type, $options{'node'});
  exit 0;
}
elsif($options{'faston'}) {
  action("$url", "Fast On", $type, $options{'node'});
  exit 0;
}
elsif($options{'fastoff'}) {
  action("$url", "Fast Off", $type, $options{'node'});
  exit 0;
}
elsif($options{'dim'}) {
  action("$url", "Dim", $type, $options{'node'});
  exit 0;
}
elsif($options{'brighten'}) {
  action("$url", "Brighten", $type, $options{'node'});
  exit 0;
}





# If we didn't do anything, print usage
print "No command specified\n";
pod2usage(1);
exit 1;

[/code]

Posted

And for completeness, here is a bash script that queries a device (cant query scenes with this) for its status, and sends a scene control based on that. Result is a scene toggle. If its on, turn it off. If its off turn it on.

 

The reason I didn't include that above is that there are likely to be lots of crazy cases. For example i have some here where any of 2 devices could be on which I'd want to indicate the scene is "on" and turn them all off. Easiest to just do that in bash as need be.

 

#!/bin/bash
#
ISYPL="/usr/local/bin/isy.pl"
ISYCFILE="/secret/isy_password"
HOST="isyhostname"

STATUS=`$ISYPL -host "$HOST" -cfile "$ISYCFILE" -node "5 94 76 1" -query`

if [ "$STATUS" == "Off" ]; then
   echo "Light is Off. Turning scene on"
   $ISYPL -host $HOST -cfile $ISYCFILE -type scenes -node "21668" -on
else
   echo "Light is not off. Turning scene off."
   $ISYPL -host $HOST -cfile $ISYCFILE -type scenes -node "21668" -off
fi

Posted
Hello rubin,

May I humbly request that you also post this article in the Developer Forum under WSDK:

http://forum.universal-devices.com/viewforum.php?f=62

 

Would this be appropriate there?, It doesn't USE the WSDK, it's just scraping the isy website. (Forgive my ignorance, is WSDK the same as the REST interface on the 99s?)

 

Is there a better interface for doing this on the -26es? I was under the impression there wasn't. What did I miss?

Posted

Rubin,

 

Would you mind posting a link to the updated script? The inline code is somehow getting mangled when I copy/paste from your post.

 

Thanks,

-Jim

Posted
Rubin,

 

Would you mind posting a link to the updated script? The inline code is somehow getting mangled when I copy/paste from your post.

 

Thanks,

-Jim

 

Sure,

I updated the script in the link.

-Alex

Guest
This topic is now closed to further replies.

×
×
  • Create New...