#!/usr/bin/perl use Data::Dumper; use HTTP::Tiny; use JSON::PP; use feature qw(switch); use FindBin; # find the script's directory use lib $FindBin::Bin; # add that directory to the library path use xPL::common; ################################################################################ # constants # $vendor_id = 'dspc'; # from xplproject.org $device_id = 'hue'; # max 8 chars $class_id = 'lighting'; # max 8 chars $separator = '-' x 80; $indent = ' ' x 2; #------------------------------------------------------------------------------- # global variables # my %configuration; $configuration{'bridge'} = 'apollo.cofnet'; $configuration{'user'} = 'xplHueDevice'; ################################################################################ # Input arguments # use Getopt::Std; my %opts; getopts('hvp:n:t:w:b:u:', \%opts); die("\n". "Usage: $0 [options]\n". "\n". "Options:\n". "${indent}-h display this help message\n". "${indent}-v verbose\n". "${indent}-p port the base UDP port\n". "${indent}-n id the instance id (max. 12 chars)\n". "${indent}-t mins the heartbeat interval in minutes\n". "${indent}-w secs the startup sleep interval\n". "${indent}-b bridge the IP address of the hue bridge\n". "${indent}-u name a username in the hue bridge whitelist\n". "\n". "Controls a Philips hue lighting system.\n". "\n". "More information with: perldoc $0\n". "\n". "" ) if ($opts{h}); my $verbose = $opts{v}; my $client_base_port = $opts{'p'} || 50000; my $instance_id = $opts{'n'} || xpl_build_automatic_instance_id; my $heartbeat_interval = $opts{'t'} || 5; my $startup_sleep_time = $opts{'w'} || 0; $configuration{'bridge'} = $opts{'b'} || $configuration{'bridge'}; $configuration{'user'} = $opts{'u'} || $configuration{'user'}; ################################################################################ # Internal functions # #------------------------------------------------------------------------------- # Send JSON message to RESTful web service # sub restful_send { my ($URL, $method, $body) = @_; # send message my $response = HTTP::Tiny->new->request($method, $URL, {content => $body}); #print Dumper($response); # get answer $response = $response->{'content'}; return($response) } #------------------------------------------------------------------------------- # Translate an xPL message to the corresponding hue JSON message # sub build_hue_message { my (%body) = @_; my %message = (); my $deviceId = ''; # interpret commands foreach $control (keys(%body)) { #print "$control: $body{$control}\n"; if (lc($control) eq 'command') { if (lc($body{$control}) eq 'activate') { $message{'on'} = 'true'; } elsif (lc($body{$control}) eq 'deactivate') { $message{'on'} = 'false'; } } elsif (lc($control) eq 'device') { $deviceId = $body{$control}; } elsif (lc($control) eq 'level') { $message{'bri'} = int($body{$control} * 255/100 + 0.5); } elsif (lc($control) eq 'color') { $message{'hue'} = $body{$control}; given ($body{$control}) { when ('red') { $message{'hue'} = int( 0 * 2**16/360 + 0.5) } when ('orange') { $message{'hue'} = int( 30 * 2**16/360 + 0.5) } when ('yellow') { $message{'hue'} = int( 60 * 2**16/360 + 0.5) } when ('chartreuse') { $message{'hue'} = int( 90 * 2**16/360 + 0.5) } when ('green') { $message{'hue'} = int(120 * 2**16/360 + 0.5) } when ('springgreen') { $message{'hue'} = int(150 * 2**16/360 + 0.5) } when ('cyan') { $message{'hue'} = int(180 * 2**16/360 + 0.5) } when ('aqua') { $message{'hue'} = int(180 * 2**16/360 + 0.5) } when ('slateblue') { $message{'hue'} = int(210 * 2**16/360 + 0.5) } when ('blue') { $message{'hue'} = int(240 * 2**16/360 + 0.5) } when ('slateblue2') { $message{'hue'} = int(270 * 2**16/360 + 0.5) } when ('magenta') { $message{'hue'} = int(300 * 2**16/360 + 0.5) } } } elsif (lc($control) eq 'hue') { $message{'hue'} = int($body{$control} * 2**16/360 + 0.5); } elsif (lc($control) eq 'saturation') { $message{'sat'} = int($body{$control} * 255/100 + 0.5); } elsif (lc($control) eq 'fade-rate') { $message{'transitiontime'} = 10 * $body{$control}; } } # encode to JSON string my $message = encode_json(\%message); $message =~ s/"false"/false/g; $message =~ s/"true"/true/g; $message =~ s/"(\d+)"/$1/g; return($deviceId, $message) } #------------------------------------------------------------------------------- # Translate a serial port message to the corresponding status # sub build_status { my ($deviceId, $message) = @_; my %status; # decode JSON string my $hue_status = decode_json($message); #print Dumper($hue_status); # trim information $status{'device'} = $deviceId; $status{'name'} = $hue_status->{'name'}; $status{'hue'} = $hue_status->{'state'}->{'hue'}; $status{'brightness'} = $hue_status->{'state'}->{'bri'}; $status{'saturation'} = $hue_status->{'state'}->{'sat'}; $status{'name'} =~ s/\s+/_/g; return(%status) } ################################################################################ # Catch control-C interrupt # $SIG{INT} = sub{ $xpl_end++ }; ################################################################################ # Main script # sleep($startup_sleep_time); # xPL parameters my $xpl_id = xpl_build_id($vendor_id, $device_id, $instance_id); my $xpl_ip = xpl_find_ip; # create xPL socket my ($client_port, $xpl_socket) = xpl_open_socket($xpl_port, $client_base_port); # display working parameters if ($verbose == 1) { system("clear"); print("$separator\n"); print("Controlling a Philips hue via bridge at \"$configuration{'bridge'}\".\n"); print($indent . "class id : $class_id\n"); print($indent . "instance id: $instance_id\n"); print($indent . "user : $configuration{'user'}\n"); print("\n"); } #------------------------------------------------------------------------------- # Main loop # my $timeout = 1; my $last_heartbeat_time = 0; while ( (defined($xpl_socket)) && ($xpl_end == 0) ) { # check time and send heartbeat $last_heartbeat_time = xpl_send_heartbeat( $xpl_socket, $xpl_id, $xpl_ip, $client_port, $heartbeat_interval, $last_heartbeat_time ); # get xpl-UDP message with timeout my ($xpl_message) = xpl_get_message($xpl_socket, $timeout); # process XPL message if ($xpl_message) { my ($type, $source, $target, $schema, %body) = xpl_get_message_elements($xpl_message); if ( xpl_is_for_me($xpl_id, $target) ) { # process commands if ($schema eq "$class_id.basic") { if ($type eq 'xpl-cmnd') { if ($verbose > 0) { print("\n"); print("Received command from \"$source\"\n"); } my ($deviceId, $command) = build_hue_message(%body); #print "command: $command\n"; if ( ($deviceId ne '') and ($command ne '{}') ) { if ($verbose == 1) { print $indent, "sending: $command to bulb $deviceId\n"; } my $URL = "http://$configuration{'bridge'}/api/$configuration{'user'}"; $URL .= "/lights/$deviceId/state"; restful_send($URL, 'PUT', $command); } } } # process replies if ($schema eq "$class_id.request") { if ($type eq 'xpl-cmnd') { if ($verbose == 1) { print "\n"; print("Received request from \"$source\"\n"); } my $deviceId = $body{'device'}; my $URL = "http://$configuration{'bridge'}/api/$configuration{'user'}"; $URL .= "/lights/$deviceId"; my $status = restful_send($URL, 'GET', $command); #print "-> $status\n"; my %body = build_status($deviceId, $status); if ($verbose == 1) { my $status = encode_json(\%body); print $indent, "replying: $status\n"; } xpl_send_message( $xpl_socket, $xpl_port, 'xpl-stat', $xpl_id, '*', "$class_id.status", %body ); } } } } } xpl_disconnect($xpl_socket, $xpl_id, $xpl_ip, $client_port); ################################################################################ # Documentation (access it with: perldoc ) # __END__ =head1 NAME xpl-hue.pl - Controls a Philips hue lighting system =head1 SYNOPSIS xpl-hue.pl [options] =head1 DESCRIPTION This xPL client sends commands to a hue bridge in order to control light bulbs. The C commands allow to control following items: =over 8 =item B The device id. =item B Can be C or C. =item B Can be C, C, C, C, C, C, C, C, C, C, C or C. =item B Can vary from 0 to 100. =item B Can vary from 0 to 359. =item B Can vary from 0 to 100. =item B In seconds. Can vary from 1 to 6553 (109 min.). =back =head1 OPTIONS =over 8 =item B<-h> Display a help message. =item B<-v> Be verbose. =item B<-p port> Specify the base port from which the client searches for a free port. If not specified, the client will take a default value. =item B<-n id> Specify the instance id (name). The id is limited to 12 characters. If not specified, it is constructed from the host name. =item B<-t mins> Specify the number of minutes between two heartbeat messages. =item B<-w secs> Specify the number of seconds before sending the first heartbeat. This allows to start the client after the hub, thus eliminating an prospective startup delay of one heartbeat interval. =item B<-b str> Specify the hue bridge's URL. =item B<-u str> Specify a username in the bridge's whitelist. =back =head1 TEST Make sure you have an C running on the machine. Start the hue controller: C. Start C in another terminal window. Launch the command C. The bulb nb 3 should turn on and C display the control message. Launch the command C. C should display the status message. =head1 AUTHOR Francois Corthay, DSPC =head1 VERSION 1.1, 2014 =cut