rewrite to Perl, single file
This commit is contained in:
parent
b464c7adfe
commit
25428cf5ed
267
huectl.pl
Executable file
267
huectl.pl
Executable file
@ -0,0 +1,267 @@
|
|||||||
|
#!/usr/bin/env perl
|
||||||
|
#
|
||||||
|
# Copyright 2020, Mischa Peters <mischa AT high5 DOT nl>, 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);
|
Loading…
Reference in New Issue
Block a user