commit 25428cf5edb3a454955867e349a7b17a64c04692 from: mischa date: Wed Jun 24 19:04:13 2020 UTC rewrite to Perl, single file commit - b464c7adfe8f1f190a86699d70938932cb9c2117 commit + 25428cf5edb3a454955867e349a7b17a64c04692 blob - /dev/null blob + 7d13a61b9a398e25e06f1c92dd9c15c1be322b43 (mode 755) --- /dev/null +++ huectl.pl @@ -0,0 +1,267 @@ +#!/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), + "action=s" => \(my $ACTION = "state"), + "verbose" => \(my $VERBOSE), + "debug" => \(my $DEBUG), +); + +my $USAGE = <<"END_USAGE"; +Usage: $0 bridge-name [-t type] [-i id] [-s sensor] [-b percent] [-a action] [-v] [-d] +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 +-a | --action [ on | off | state | bright | relax | morning | dimmed | evening | nightlight ] (default: state) +-v | --verbose JSON output +-d | --debug 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 + +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 = map { -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("Error: bridge-name '$bridgename' not found.\n\n$USAGE"); +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/ && $VERBOSE) { + print "URI: $uri\nHTTP RESPONSE: $status\nCONTENT: \n$content\n"; + } + say $json->ascii->pretty->encode(decode_json join '', $content) if $DEBUG; + 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; + 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) { + 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/) { + 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'}; + } else { + 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'}; + } + } + } + } + } +} + +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); + 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);