Blob


1 #!/usr/bin/env perl
2 #
3 # Copyright 2020, Mischa Peters <mischa AT high5 DOT nl>, High5!.
4 # Version 0.9 - 20200624
5 #
6 # Follow the steps at the Hue Developer site to get the username/token
7 # https://developers.meethue.com/develop/get-started-2/
8 #
9 use 5.024;
10 use strict;
11 use warnings;
12 use autodie;
13 use Getopt::Long;
14 use Config::Tiny;
15 use HTTP::Tiny;
16 use JSON::PP;
18 GetOptions(
19 "type=s" => \(my $TYPE = "lights"),
20 "id=i" => \(my $RESOURCE_ID),
21 "sensor=i" => \(my $SENSOR_ID),
22 "battery=i" => \(my $BATTERY),
23 "climate" => \(my $CLIMATE),
24 "action=s" => \(my $ACTION = "state"),
25 "verbose" => \(my $VERBOSE),
26 "debug" => \(my $DEBUG),
27 "pretty" => \(my $PRETTY),
28 );
30 my $USAGE = <<"END_USAGE";
31 Usage: $0 bridge-name [-t type] [-i id] [-s sensor] [-b percent] [-a action] [-v] [-d] [-p]
32 Options:
33 bridge-name as defined in [HOME]./hue.conf or [HOME]./.hue.conf or /etc/hue.conf
34 -t | --type [ lights | sensors | groups | all | trigger ] (default: lights)
35 -i | --id light-id
36 -s | --sensor sensor-id
37 -b | --battery percent of battery level to report on, only relevant with sensors
38 -c | --climate show temperature of sensors in C, only relevant with sensors
39 -a | --action [ on | off | state | bright | relax | morning | dimmed | evening | nightlight ] (default: state)
40 -v | --verbose
41 -d | --debug JSON output
42 -p | --pretty pretty JSON output (can be a lot)
44 Command examples:
45 $0 bridge1
46 Displays all lights of bridge1
47 $0 bridge1 -i 8
48 Check for state of light-id 8
49 $0 bridge2 -t lights -i 8 -a bright
50 Turn on light-id 8 with the scene bright
51 $0 bridge2 -t trigger -i 8 -s 34 -a evening
52 Check for 'dark' state of sensor-id 34, turn on light-id 8 with the scene evening
53 $0 bridge1 -t sensors -h
54 Displays temperature of all sensors in C
56 Config example:
57 # huectl,pl config file locations:
58 # ~/hue.conf, ~/.hue.conf, /etc/hue.conf, ./.hue.conf, ./hue.conf
59 [bridge1]
60 ip = 192.168.100.101
61 token = bridge1token
62 [bridge2]
63 ip = 192.168.100.102
64 token = bridge2token
65 END_USAGE
67 my ($bridgename) = @ARGV;
68 if (!$bridgename) { _return_error_with($USAGE); }
70 my @config_files = map { -e $_ ? $_ : () } ('./hue.conf', './.hue.conf', '/etc/hue.conf', "$ENV{'HOME'}/.hue.conf", "$ENV{'HOME'}/hue.conf");
71 my $config = Config::Tiny->read($config_files[-1], 'utf8');
72 my $bridge = $config->{$bridgename}{ip} || _return_error_with("$USAGE\nError: bridge-name '$bridgename' not found.\n");
73 my $token = $config->{$bridgename}{token};
74 my $http = HTTP::Tiny->new;
75 my $json = JSON::PP->new;
76 my $base_uri = "https://$bridge/api/$token";
78 my %scenes;
79 $scenes{'br'}{'bright'} = qq{{"on": true, "bri": 254, "alert": "none"}};
80 $scenes{'ct'}{'bright'} = qq{{"on": true, "bri": 254, "ct": 367, "alert": "none"}};
81 $scenes{'xy'}{'bright'} = qq{{"on": true, "bri": 254, "ct": 367, "alert": "none", "hue": 8402, "sat": 140, "effect": "none", "xy": [0.4578, 0.41]}};
82 $scenes{'br'}{'relax'} = qq{{"on": true, "bri": 144, "alert": "none"}};
83 $scenes{'ct'}{'relax'} = qq{{"on": true, "bri": 144, "ct": 447, "alert": "none"}};
84 $scenes{'xy'}{'relax'} = qq{{"on": true, "bri": 144, "ct": 447, "alert": "none", "hue": 8402, "sat": 140, "effect": "none", "xy": [0.5019, 0.4152]}};
85 $scenes{'br'}{'morning'} = qq{{"on": true, "bri": 100, "alert": "none"}};
86 $scenes{'ct'}{'morning'} = qq{{"on": true, "bri": 100, "ct": 447, "alert": "none"}};
87 $scenes{'xy'}{'morning'} = qq{{"on": true, "bri": 100, "ct": 447, "alert": "none", "hue": 8402, "sat": 140, "effect": "none", "xy": [0.5019, 0.4152]}};
88 $scenes{'br'}{'dimmed'} = qq{{"on": true, "bri": 77, "alert": "none"}};
89 $scenes{'ct'}{'dimmed'} = qq{{"on": true, "bri": 77, "ct": 367, "alert": "none"}};
90 $scenes{'xy'}{'dimmed'} = qq{{"on": true, "bri": 77, "ct": 367, "alert": "none", "hue": 8402, "sat": 140, "effect": "none", "xy": [0.4578, 0.41]}};
91 $scenes{'br'}{'evening'} = qq{{"on": true, "bri": 63, "alert": "none"}};
92 $scenes{'ct'}{'evening'} = qq{{"on": true, "bri": 63, "ct": 447, "alert": "none"}};
93 $scenes{'xy'}{'evening'} = qq{{"on": true, "bri": 63, "ct": 447, "alert": "none", "hue": 8402, "sat": 140, "effect": "none", "xy": [0.5019, 0.4152]}};
94 $scenes{'br'}{'nightlight'} = qq{{"on": true, "bri": 1, "alert": "none"}};
95 $scenes{'ct'}{'nightlight'} = qq{{"on": true, "bri": 1, "ct": 447, "alert": "none"}};
96 $scenes{'xy'}{'nightlight'} = qq{{"on": true, "bri": 1, "ct": 367, "alert": "none", "hue": 8402, "sat": 140, "effect": "none", "xy": [0.561, 0.4042]}};
98 sub _return_error_with {
99 my ($message) = @_;
100 say "$message";
101 exit 1;
104 sub _verify_response {
105 my ($status, $content, $uri) = @_;
106 if ($status =~ /^2/) {
107 print "URI: $uri\nHTTP RESPONSE: $status\n" if ($VERBOSE || $DEBUG);
108 print "CONTENT: \n$content\n" if $DEBUG;
110 say $json->ascii->pretty->encode(decode_json join '', $content) if $PRETTY;
111 if ($status !~ /^2/ || $content =~ /error/) {
112 _return_error_with("URI: $uri\nHTTP RESPONSE: $status\nCONTENT: \n$content");
116 sub _get_json_for {
117 my ($resource, $resource_id) = @_;
118 my $uri = "$base_uri/$resource";
119 $uri .= "/$resource_id" if $resource_id;
120 my $response = $http->get($uri);
121 _verify_response($response->{'status'}, $response->{'content'}, $uri);
122 return $json->decode($response->{'content'});
125 sub _put_json_body {
126 my ($resource, $resource_id, $body) = @_;
127 my $uri = "$base_uri/$resource/$resource_id/state";
128 my $response = $http->put($uri, {'content' => $body});
129 _verify_response($response->{'status'}, $response->{'content'}, $uri);
130 return $json->decode($response->{'content'});
133 sub _get_state_for {
134 my ($resource, $resource_id) = @_;
135 my $data = _get_json_for($resource, $resource_id);
136 if ($data->{'state'}) {
137 return $data->{'state'};
141 sub _change_state_for {
142 my ($resource, $resource_id, $ACTION) = @_;
143 my $resource_state = _get_state_for($resource, $resource_id);
144 my $colormode;
145 my $light_attributes;
146 if (! $resource_state->{'colormode'}) {
147 $colormode = 'br';
149 else {
150 $colormode = $resource_state->{'colormode'};
152 if ($ACTION eq 'off') {
153 $light_attributes = qq{{"on": false}};
155 elsif ($ACTION eq 'on') {
156 $light_attributes = qq{{"on": true}};
158 elsif (exists($scenes{$colormode}{$ACTION})) {
159 $light_attributes = $scenes{$colormode}{$ACTION};
161 _put_json_body($resource, $resource_id, $light_attributes);
164 sub lights {
165 my ($resource) = @_;
166 if (! $RESOURCE_ID) {
167 my $light_objects = _get_json_for($resource);
168 my $state;
169 printf "%4s %-34s %-8s %s (%s)\n", "ID", "Name", "State", "Type", $TYPE;
170 print "################################################################################\n";
171 for my $key (sort { $a <=> $b } keys (%{$light_objects})) {
172 if ($light_objects->{$key}->{'state'}->{'reachable'}) {
173 $state = $light_objects->{$key}->{'state'}->{'on'} ? "on" : "off";
175 else {
176 $state = "N/A";
178 printf "%4d %-34s %-8s %s\n", $key, $light_objects->{$key}->{'name'}, $state, $light_objects->{$key}->{'type'};
181 else {
182 if ($ACTION ne "state") {
183 _change_state_for($resource, $RESOURCE_ID, $ACTION);
185 else {
186 my $light_state = _get_state_for($resource, $RESOURCE_ID);
187 if ($light_state->{'reachable'}) {
188 say $light_state->{'on'} ? "on" : "off";
190 else {
191 say "unreachable";
197 sub sensors {
198 my ($resource) = @_;
199 my $sensor_objects = _get_json_for($resource);
200 my %sensor;
201 my $name;
202 my $temperature;
203 UNIQUEID:
204 for my $key (keys (%{$sensor_objects})) {
205 if ($sensor_objects->{$key}->{'uniqueid'} && ($sensor_objects->{$key}->{'uniqueid'} =~ /([a-fA-F0-9]{2}:?){8}/)) {
206 next UNIQUEID if ($sensor_objects->{$key}->{'type'} =~ m/ZGPSwitch/);
207 # Strip first 23 characters from uniqueid, push key in array in hash
208 push (@{$sensor{ unpack('@0 A23', $sensor_objects->{$key}->{'uniqueid'}) }}, $key);
212 if (! $BATTERY && ! $CLIMATE) {
213 printf "%4s %-34s %-8s %s (%s)\n", "ID", "Name", "State", "Type", $TYPE;
214 print "################################################################################\n";
216 for my $uniqueid (sort keys %sensor) {
217 for my $key (sort { $a <=> $b } @{$sensor{$uniqueid}}) {
218 if ($BATTERY) {
219 if ($sensor_objects->{$key}->{'type'} =~ /ZLLSwitch|ZLLPresence/) {
220 if ($sensor_objects->{$key}->{'config'}->{'battery'} < $BATTERY) {
221 printf "%-32s battery level %s%%\n", $sensor_objects->{$key}->{'name'}, $sensor_objects->{$key}->{'config'}->{'battery'};
225 elsif ($CLIMATE) {
226 if ($sensor_objects->{$key}->{'type'} =~ /ZLLPresence/) {
227 $name = $sensor_objects->{$key}->{'name'};
229 if ($sensor_objects->{$key}->{'type'} =~ /ZLLTemperature/) {
230 if ($sensor_objects->{$key}->{'state'}->{'temperature'}) {
231 $temperature = ($sensor_objects->{$key}->{'state'}->{'temperature'} / 100);
232 printf "%-32s - %.1fC", $name, $temperature;
233 print " (updated: " . unpack('@11 A8', $sensor_objects->{$key}->{'state'}->{'lastupdated'}) . " UTC)";
234 print " - sensor $key" if $VERBOSE;
235 say "";
239 else {
240 if ($sensor_objects->{$key}->{'type'} =~ /ZLLSwitch|ZLLPresence/) {
241 printf "%-39s (%s%%)\n", $sensor_objects->{$key}->{'name'}, $sensor_objects->{$key}->{'config'}->{'battery'};
243 printf "%4d %-43s %s\n", $key, $sensor_objects->{$key}->{'productname'}, $sensor_objects->{$key}->{'type'};
249 sub groups {
250 my ($resource) = @_;
251 my $group_objects = _get_json_for($resource);
252 printf "%4s %-34s %-8s %-8s %6s %s (%s)\n", "ID", "Name", "All On", "Any On", "Lights", "Type", $TYPE;
253 print "################################################################################\n";
254 for my $key (sort { $a <=> $b } keys (%{$group_objects})) {
255 my $all_on = $group_objects->{$key}->{'state'}->{'all_on'} ? "yes" : "no";
256 my $any_on = $group_objects->{$key}->{'state'}->{'any_on'} ? "yes" : "no";
257 my $light_count = scalar @{$group_objects->{$key}->{'lights'}};
258 printf "%4d %-34s %-8s %-8s %-6d %s\n", $key, $group_objects->{$key}->{'name'}, $all_on, $any_on, $light_count, $group_objects->{$key}->{'type'};
262 sub daylight_trigger {
263 my $light = _get_state_for("lights", $RESOURCE_ID);
264 my $sensor = _get_state_for("sensors", $SENSOR_ID);
265 say "Dark: $sensor->{'dark'}, Daylight: $sensor->{'daylight'}, Light On: $light->{'on'}" if ($VERBOSE || $DEBUG);
266 #print (f"Dark: {sensor_state['dark']}, Daylight: {sensor_state['daylight']}, Light On: {sensor_state['daylight']}")
267 if ($sensor->{'dark'} && ! $light->{'on'}) {
268 _change_state_for("lights", $RESOURCE_ID, $ACTION);
270 if (! $sensor->{'dark'} && $light->{'on'}) {
271 _change_state_for("lights", $RESOURCE_ID, "off");
275 sub get_all {
276 lights("lights");
277 say "";
278 sensors("sensors");
279 say "";
280 groups("groups");
281 say "";
284 my $dispatch_for = {
285 'all' => \&get_all,
286 'lights' => \&lights,
287 'sensors' => \&sensors,
288 'groups' => \&groups,
289 'trigger' => \&daylight_trigger,
290 'DEFAULT' => sub { say "$USAGE"; },
291 };
292 my $func = $dispatch_for->{$TYPE} || $dispatch_for->{DEFAULT};
293 $func->($TYPE);