#!/usr/bin/env perl # # Copyright 2020, Mischa Peters , High5!. # Version 0.9 - 20200624 # # Follow the steps at the Hue Developer site to get the username/token # https://developers.meethue.com/develop/get-started-2/ # use 5.024; use strict; use warnings; use autodie; use Getopt::Long; use Config::Tiny; use HTTP::Tiny; use JSON::PP; GetOptions( "type=s" => \(my $TYPE = "lights"), "id=i" => \(my $RESOURCE_ID), "sensor=i" => \(my $SENSOR_ID), "battery=i" => \(my $BATTERY), "climate" => \(my $CLIMATE), "action=s" => \(my $ACTION = "state"), "verbose" => \(my $VERBOSE), "debug" => \(my $DEBUG), "pretty" => \(my $PRETTY), ); my $USAGE = <<"END_USAGE"; Usage: $0 bridge-name [-t type] [-i id] [-s sensor] [-b percent] [-a action] [-v] [-d] [-p] Options: bridge-name as defined in [HOME]./hue.conf or [HOME]./.hue.conf or /etc/hue.conf -t | --type [ lights | sensors | groups | all | trigger ] (default: lights) -i | --id light-id -s | --sensor sensor-id -b | --battery percent of battery level to report on, only relevant with sensors -c | --climate show temperature of sensors in C, only relevant with sensors -a | --action [ on | off | state | bright | relax | morning | dimmed | evening | nightlight ] (default: state) -v | --verbose -d | --debug raw JSON output -p | --pretty pretty JSON output Command examples: $0 bridge1 Displays all lights of bridge1 $0 bridge1 -i 8 Check for state of light-id 8 $0 bridge2 -t lights -i 8 -a bright Turn on light-id 8 with the scene bright $0 bridge2 -t trigger -i 8 -s 34 -a evening Check for 'dark' state of sensor-id 34, turn on light-id 8 with the scene evening $0 bridge1 -t sensors -c Displays temperature of all sensors in C Config example: # huectl,pl config file locations: # ~/hue.conf, ~/.hue.conf, /etc/hue.conf, ./.hue.conf, ./hue.conf [bridge1] ip = 192.168.100.101 token = bridge1token [bridge2] ip = 192.168.100.102 token = bridge2token END_USAGE my ($bridgename) = @ARGV; if (!$bridgename) { _return_error_with($USAGE); } my @config_files = grep { -e } ('./hue.conf', './.hue.conf', '/etc/hue.conf', "$ENV{'HOME'}/.hue.conf", "$ENV{'HOME'}/hue.conf"); my $config = Config::Tiny->read($config_files[-1], 'utf8'); my $bridge = $config->{$bridgename}{ip} || _return_error_with("$USAGE\nError: bridge-name '$bridgename' not found.\n"); my $token = $config->{$bridgename}{token}; my $http = HTTP::Tiny->new; my $json = JSON::PP->new; my $base_uri = "https://$bridge/api/$token"; my %scenes; $scenes{'br'}{'bright'} = qq{{"on": true, "bri": 254, "alert": "none"}}; $scenes{'ct'}{'bright'} = qq{{"on": true, "bri": 254, "ct": 367, "alert": "none"}}; $scenes{'xy'}{'bright'} = qq{{"on": true, "bri": 254, "ct": 367, "alert": "none", "hue": 8402, "sat": 140, "effect": "none", "xy": [0.4578, 0.41]}}; $scenes{'br'}{'relax'} = qq{{"on": true, "bri": 144, "alert": "none"}}; $scenes{'ct'}{'relax'} = qq{{"on": true, "bri": 144, "ct": 447, "alert": "none"}}; $scenes{'xy'}{'relax'} = qq{{"on": true, "bri": 144, "ct": 447, "alert": "none", "hue": 8402, "sat": 140, "effect": "none", "xy": [0.5019, 0.4152]}}; $scenes{'br'}{'morning'} = qq{{"on": true, "bri": 100, "alert": "none"}}; $scenes{'ct'}{'morning'} = qq{{"on": true, "bri": 100, "ct": 447, "alert": "none"}}; $scenes{'xy'}{'morning'} = qq{{"on": true, "bri": 100, "ct": 447, "alert": "none", "hue": 8402, "sat": 140, "effect": "none", "xy": [0.5019, 0.4152]}}; $scenes{'br'}{'dimmed'} = qq{{"on": true, "bri": 77, "alert": "none"}}; $scenes{'ct'}{'dimmed'} = qq{{"on": true, "bri": 77, "ct": 367, "alert": "none"}}; $scenes{'xy'}{'dimmed'} = qq{{"on": true, "bri": 77, "ct": 367, "alert": "none", "hue": 8402, "sat": 140, "effect": "none", "xy": [0.4578, 0.41]}}; $scenes{'br'}{'evening'} = qq{{"on": true, "bri": 63, "alert": "none"}}; $scenes{'ct'}{'evening'} = qq{{"on": true, "bri": 63, "ct": 447, "alert": "none"}}; $scenes{'xy'}{'evening'} = qq{{"on": true, "bri": 63, "ct": 447, "alert": "none", "hue": 8402, "sat": 140, "effect": "none", "xy": [0.5019, 0.4152]}}; $scenes{'br'}{'nightlight'} = qq{{"on": true, "bri": 1, "alert": "none"}}; $scenes{'ct'}{'nightlight'} = qq{{"on": true, "bri": 1, "ct": 447, "alert": "none"}}; $scenes{'xy'}{'nightlight'} = qq{{"on": true, "bri": 1, "ct": 367, "alert": "none", "hue": 8402, "sat": 140, "effect": "none", "xy": [0.561, 0.4042]}}; sub _return_error_with { my ($message) = @_; say "$message"; exit 1; } sub _verify_response { my ($status, $content, $uri) = @_; if ($status =~ /^2/) { print "URI: $uri\nHTTP RESPONSE: $status\n" if ($VERBOSE || $DEBUG); print "CONTENT: \n$content\n" if $DEBUG; } say $json->ascii->pretty->encode(decode_json join '', $content) if $PRETTY; if ($status !~ /^2/ || $content =~ /error/) { _return_error_with("URI: $uri\nHTTP RESPONSE: $status\nCONTENT: \n$content"); } } sub _get_json_for { my ($resource, $resource_id) = @_; my $uri = "$base_uri/$resource"; $uri .= "/$resource_id" if $resource_id; my $response = $http->get($uri); _verify_response($response->{'status'}, $response->{'content'}, $uri); return $json->decode($response->{'content'}); } sub _put_json_body { my ($resource, $resource_id, $body) = @_; my $uri = "$base_uri/$resource/$resource_id/state"; my $response = $http->put($uri, {'content' => $body}); _verify_response($response->{'status'}, $response->{'content'}, $uri); return $json->decode($response->{'content'}); } sub _get_state_for { my ($resource, $resource_id) = @_; my $data = _get_json_for($resource, $resource_id); if ($data->{'state'}) { return $data->{'state'}; } } sub _change_state_for { my ($resource, $resource_id, $ACTION) = @_; my $resource_state = _get_state_for($resource, $resource_id); my $colormode; my $light_attributes; if (! $resource_state->{'colormode'}) { $colormode = 'br'; } else { $colormode = $resource_state->{'colormode'}; } if ($ACTION eq 'off') { $light_attributes = qq{{"on": false}}; } elsif ($ACTION eq 'on') { $light_attributes = qq{{"on": true}}; } elsif (exists($scenes{$colormode}{$ACTION})) { $light_attributes = $scenes{$colormode}{$ACTION}; } _put_json_body($resource, $resource_id, $light_attributes); } sub lights { my ($resource) = @_; if (! $RESOURCE_ID) { my $light_objects = _get_json_for($resource); my $state; printf "%4s %-34s %-8s %s (%s)\n", "ID", "Name", "State", "Type", $TYPE; print "################################################################################\n"; for my $key (sort { $a <=> $b } keys (%{$light_objects})) { if ($light_objects->{$key}->{'state'}->{'reachable'}) { $state = $light_objects->{$key}->{'state'}->{'on'} ? "on" : "off"; } else { $state = "N/A"; } printf "%4d %-34s %-8s %s\n", $key, $light_objects->{$key}->{'name'}, $state, $light_objects->{$key}->{'type'}; } } else { if ($ACTION ne "state") { _change_state_for($resource, $RESOURCE_ID, $ACTION); } else { my $light_state = _get_state_for($resource, $RESOURCE_ID); if ($light_state->{'reachable'}) { say $light_state->{'on'} ? "on" : "off"; } else { say "unreachable"; } } } } sub sensors { my ($resource) = @_; my $sensor_objects = _get_json_for($resource); my %sensor; my $name; my $temperature; UNIQUEID: for my $key (keys (%{$sensor_objects})) { if ($sensor_objects->{$key}->{'uniqueid'} && ($sensor_objects->{$key}->{'uniqueid'} =~ /([a-fA-F0-9]{2}:?){8}/)) { next UNIQUEID if ($sensor_objects->{$key}->{'type'} =~ m/ZGPSwitch/); # Strip first 23 characters from uniqueid, push key in array in hash push (@{$sensor{ unpack('@0 A23', $sensor_objects->{$key}->{'uniqueid'}) }}, $key); } } if (! $BATTERY && ! $CLIMATE) { printf "%4s %-34s %-8s %s (%s)\n", "ID", "Name", "State", "Type", $TYPE; print "################################################################################\n"; } for my $uniqueid (sort keys %sensor) { for my $key (sort { $a <=> $b } @{$sensor{$uniqueid}}) { if ($BATTERY) { if ($sensor_objects->{$key}->{'type'} =~ /ZLLSwitch|ZLLPresence/) { if ($sensor_objects->{$key}->{'config'}->{'battery'} < $BATTERY) { printf "%-32s battery level %s%%\n", $sensor_objects->{$key}->{'name'}, $sensor_objects->{$key}->{'config'}->{'battery'}; } } } elsif ($CLIMATE) { if ($sensor_objects->{$key}->{'type'} =~ /ZLLPresence/) { $name = $sensor_objects->{$key}->{'name'}; } if ($sensor_objects->{$key}->{'type'} =~ /ZLLTemperature/) { if ($sensor_objects->{$key}->{'state'}->{'temperature'}) { $temperature = ($sensor_objects->{$key}->{'state'}->{'temperature'} / 100); printf "%-32s - %.1fC", $name, $temperature; print " (updated: " . unpack('@11 A8', $sensor_objects->{$key}->{'state'}->{'lastupdated'}) . " UTC)"; print " - sensor $key" if $VERBOSE; say ""; } } } else { if ($sensor_objects->{$key}->{'type'} =~ /ZLLSwitch|ZLLPresence/) { printf "%-39s (%s%%)\n", $sensor_objects->{$key}->{'name'}, $sensor_objects->{$key}->{'config'}->{'battery'}; } printf "%4d %-43s %s\n", $key, $sensor_objects->{$key}->{'productname'}, $sensor_objects->{$key}->{'type'}; } } } } sub groups { my ($resource) = @_; my $group_objects = _get_json_for($resource); printf "%4s %-34s %-8s %-8s %6s %s (%s)\n", "ID", "Name", "All On", "Any On", "Lights", "Type", $TYPE; print "################################################################################\n"; for my $key (sort { $a <=> $b } keys (%{$group_objects})) { my $all_on = $group_objects->{$key}->{'state'}->{'all_on'} ? "yes" : "no"; my $any_on = $group_objects->{$key}->{'state'}->{'any_on'} ? "yes" : "no"; my $light_count = scalar @{$group_objects->{$key}->{'lights'}}; printf "%4d %-34s %-8s %-8s %-6d %s\n", $key, $group_objects->{$key}->{'name'}, $all_on, $any_on, $light_count, $group_objects->{$key}->{'type'}; } } sub daylight_trigger { my $light = _get_state_for("lights", $RESOURCE_ID); my $sensor = _get_state_for("sensors", $SENSOR_ID); say "Dark: $sensor->{'dark'}, Daylight: $sensor->{'daylight'}, Light On: $light->{'on'}" if ($VERBOSE || $DEBUG); if ($sensor->{'dark'} && ! $light->{'on'}) { _change_state_for("lights", $RESOURCE_ID, $ACTION); } if (! $sensor->{'dark'} && $light->{'on'}) { _change_state_for("lights", $RESOURCE_ID, "off"); } } sub get_all { lights("lights"); say ""; sensors("sensors"); say ""; groups("groups"); say ""; } my $dispatch_for = { 'all' => \&get_all, 'lights' => \&lights, 'sensors' => \&sensors, 'groups' => \&groups, 'trigger' => \&daylight_trigger, 'DEFAULT' => sub { say "$USAGE"; }, }; my $func = $dispatch_for->{$TYPE} || $dispatch_for->{DEFAULT}; $func->($TYPE);