# "Hue Personal Wireless Lighting" is a trademark owned by Koninklijke Philips Electronics N.V., # see www.meethue.com for more information. # I am in no way affiliated with the Philips organization. package main; use strict; use warnings; use POSIX; use JSON; use SetExtensions; my %hueModels = ( LCT001 => {name => 'HUE Bulb' ,type => 'Extended color light' ,subType => 'colordimmer',}, LLC001 => {name => 'LivingColors G2' ,type => 'Color Light' ,subType => 'colordimmer',}, LLC005 => {name => 'LivingColors Bloom' ,type => 'Color Light' ,subType => 'colordimmer',}, LLC006 => {name => 'LivingColors Iris' ,type => 'Color Light' ,subType => 'colordimmer',}, LLC007 => {name => 'LivingColors Bloom' ,type => 'Color Light' ,subType => 'colordimmer',}, LWB001 => {name => 'LivingWhites Bulb' ,type => 'Dimmable light' ,subType => 'dimmer',}, LWL001 => {name => 'LivingWhites Outlet' ,type => 'Dimmable plug-in unit' ,subType => 'dimmer',}, ); my %dim_values = ( 0 => "dim06%", 1 => "dim12%", 2 => "dim18%", 3 => "dim25%", 4 => "dim31%", 5 => "dim37%", 6 => "dim43%", 7 => "dim50%", 8 => "dim56%", 9 => "dim62%", 10 => "dim68%", 11 => "dim75%", 12 => "dim81%", 13 => "dim87%", 14 => "dim93%", ); sub HUEDevice_Initialize($) { my ($hash) = @_; # Provide #Consumer $hash->{DefFn} = "HUEDevice_Define"; $hash->{UndefFn} = "HUEDevice_Undefine"; $hash->{SetFn} = "HUEDevice_Set"; $hash->{GetFn} = "HUEDevice_Get"; $hash->{AttrList} = "IODev ". "$readingFnAttributes ". "model:".join(",", sort keys %hueModels)." ". "subType:colordimmer,dimmer,switch"; $data{webCmdFn}{colorpicker} = "HUEDevice_colorpickerFn"; $data{FWEXT}{"/"}{SCRIPT} = "/jscolor/jscolor.js"; } sub HUEDevice_colorpickerFn($$$) { my ($FW_wname, $d, $FW_room, $cmd, $values) = @_; my @args = split("[ \t]+", $cmd); return undef if($values !~ m/^colorpicker,(.*)$/); my ($mode) = ($1); $mode = "RGB" if( !defined($mode) ); my $srf = $FW_room ? "&room=$FW_room" : ""; my $cv = CommandGet("","$d $cmd"); $cmd = "" if($cmd eq "state"); if( $args[1] ) { my $c = "\"$FW_ME?cmd=set $d $cmd$srf\""; return "". ''. '
'. '
'; ""; } else { my $c = "\"$FW_ME?cmd=set $d $cmd %$srf\""; return "". "". ""; } } sub HUEDevice_Define($$) { my ($hash, $def) = @_; my @args = split("[ \t]+", $def); return "Usage: define HUEDevice [interval]" if(@args < 3); my ($name, $type, $id, $interval) = @args; $interval= 60 unless defined($interval); if( $interval < 10 ) { $interval = 60; } $hash->{STATE} = 'Initialized'; $hash->{fhem}{interfaces}= "dimmer"; $hash->{fhem}{id} = $id; $hash->{INTERVAL} = $interval; $hash->{fhem}{on} = -1; $hash->{fhem}{colormode} = ''; $hash->{fhem}{bri} = -1; $hash->{fhem}{ct} = -1; $hash->{fhem}{hue} = -1; $hash->{fhem}{sat} = -1; $hash->{fhem}{xy} = ''; $attr{$name}{devStateIcon} = '{(CommandGet("","'.$name.' devStateIcon"),"toggle")}' if( !defined( $attr{$name}{devStateIcon} ) ); AssignIoPort($hash); if(defined($hash->{IODev}->{NAME})) { Log 3, "$name: I/O device is " . $hash->{IODev}->{NAME}; } else { Log 1, "$name: no I/O device"; } #HUEDevice_GetUpdate($hash); InternalTimer(gettimeofday()+10, "HUEDevice_GetUpdate", $hash, 0); return undef; } sub HUEDevice_Undefine($$) { my ($hash,$arg) = @_; RemoveInternalTimer($hash); delete($hash->{fhem}{id}); return undef; } sub HUEDevice_SetParam($$@) { my ($name, $obj, $cmd, $value, $value2) = @_; if( $cmd eq "color" ) { $value = int(1000000/$value); $cmd = 'ct'; } elsif( $cmd eq "toggle" ) { $cmd = ReadingsVal($name,"state","on") eq "off" ? "on" :"off"; } elsif( $cmd =~ m/^dim(\d+)/ ) { $value = $1 unless defined($value); if( $value < 0 ) { $value = 0; } if( $value > 100 ) { $value = 100; } $cmd = 'pct'; } elsif( !defined($value) && $cmd =~ m/^(\d+)/) { $value = $1; $value = 254 if( $value > 254 ); $cmd = 'bri'; } if($cmd eq 'on') { $obj->{'on'} = JSON::true; $obj->{'bri'} = 254 if( ReadingsVal($name,"bri","0") eq 0 ); $obj->{'transitiontime'} = $value / 10 if( defined($value) ); } elsif($cmd eq 'off') { $obj->{'on'} = JSON::false; $obj->{'transitiontime'} = $value / 10 if( defined($value) ); } elsif($cmd eq "pct") { $obj->{'on'} = JSON::true; $obj->{'bri'} = int(2.54 * $value); $obj->{'transitiontime'} = $value2 / 10 if( defined($value2) ); } elsif($cmd eq "bri") { $obj->{'on'} = JSON::true; $obj->{'bri'} = 0+$value; $obj->{'transitiontime'} = $value2 / 10 if( defined($value2) ); } elsif($cmd eq "ct") { $obj->{'on'} = JSON::true; $obj->{'ct'} = 0+$value; $obj->{'transitiontime'} = $value2 / 10 if( defined($value2) ); } elsif($cmd eq "hue") { $obj->{'on'} = JSON::true; $obj->{'hue'} = 0+$value; $obj->{'transitiontime'} = $value2 / 10 if( defined($value2) ); } elsif($cmd eq "sat") { $obj->{'on'} = JSON::true; $obj->{'sat'} = 0+$value; $obj->{'transitiontime'} = $value2 / 10 if( defined($value2) ); } elsif($cmd eq "xy" && $value =~ m/^(.+),(.+)/) { my ($x,$y) = ($1, $2); $obj->{'on'} = JSON::true; $obj->{'xy'} = [0+$x, 0+$y]; $obj->{'transitiontime'} = $value2 / 10 if( defined($value2) ); } elsif( $cmd eq "rgb" && $value =~ m/^(..)(..)(..)/) { # calculation from http://www.everyhue.com/vanilla/discussion/94/rgb-to-xy-or-hue-sat-values/p1 my( $r, $g, $b ) = (hex($1)/255.0, hex($2)/255.0, hex($3)/255.0); #Log 3, "rgb: ". $r . " " . $g ." ". $b; my $X = 1.076450 * $r - 0.237662 * $g + 0.161212 * $b; my $Y = 0.410964 * $r + 0.554342 * $g + 0.034694 * $b; my $Z = -0.010954 * $r - 0.013389 * $g + 1.024343 * $b; #Log 3, "XYZ: ". $X . " " . $Y ." ". $Y; if( $X != 0 || $Y != 0 || $Z != 0 ) { my $x = $X / ($X + $Y + $Z); my $y = $Y / ($X + $Y + $Z); #Log 3, "xyY:". $x . " " . $y ." ". $Y; #$x = 0 if( $x < 0 ); #$x = 1 if( $x > 1 ); #$y = 0 if( $y < 0 ); #$y = 1 if( $y > 1 ); $Y = 1 if( $Y > 1 ); my $bri = max($r,max($g,$b)); #my $bri = $Y; $obj->{'on'} = JSON::true; $obj->{'xy'} = [0+$x, 0+$y]; $obj->{'bri'} = int(254*$bri); } } elsif( $cmd eq "hsv" && $value =~ m/^(..)(..)(..)/) { my( $h, $s, $v ) = (hex($1), hex($2), hex($3)); $s = 254 if( $s > 254 ); $v = 254 if( $v > 254 ); $obj->{'on'} = JSON::true; $obj->{'hue'} = int($h*256); $obj->{'sat'} = 0+$s; $obj->{'bru'} = 0+$v; } elsif( $cmd eq "effect" ) { $obj->{'effect'} = $value; } elsif( $cmd eq "transitiontime" ) { $obj->{'transitiontime'} = 0+$value; } else { return 0; } return 1; } sub HUEDevice_Set($@); sub HUEDevice_Set($@) { my ($hash, $name, @aa) = @_; my %obj; if( (my $joined = join(" ", @aa)) =~ /:/ ) { my @cmds = split(":", $joined); for( my $i = 0; $i <= $#cmds; ++$i ) { HUEDevice_SetParam($name, \%obj, split(" ", $cmds[$i]) ); } } else { my ($cmd, $value, $value2, @a) = @aa; if( $cmd eq "statusRequest" ) { RemoveInternalTimer($hash); HUEDevice_GetUpdate($hash); return undef; } HUEDevice_SetParam($name, \%obj, $cmd, $value, $value2); } if( scalar keys %obj ) { my $result = HUEDevice_ReadFromServer($hash,$hash->{fhem}{id}."/state",\%obj); if( $result->{'error'} ) { $hash->{STATE} = $result->{'error'}->{'description'}; return undef; } $hash->{LOCAL} = 1; HUEDevice_GetUpdate($hash); delete $hash->{LOCAL}; return undef; } my $list = "off on toggle statusRequest"; $list .= " pct:slider,0,1,100 bri:slider,0,1,254" if( AttrVal($hash->{NAME}, "subType", "colordimmer") =~ m/dimmer/ ); #$list .= " dim06% dim12% dim18% dim25% dim31% dim37% dim43% dim50% dim56% dim62% dim68% dim75% dim81% dim87% dim93% dim100%" if( AttrVal($hash->{NAME}, "subType", "colordimmer") =~ m/dimmer/ ); $list .= " rgb:colorpicker,RGB color:slider,2000,1,6500 ct:slider,154,1,500 hue:slider,0,1,65535 sat:slider,0,1,254 xy effect:none,colorloop" if( AttrVal($hash->{NAME}, "subType", "colordimmer") =~ m/color/ ); return SetExtensions($hash, $list, $name, @aa); } sub HUEDevice_Get($@) { my ($hash, @a) = @_; my $name = $a[0]; return "$name: get needs at least one parameter" if(@a < 2); my $cmd= $a[1]; if($cmd eq "rgb") { my $r = 0; my $g = 0; my $b = 0; if( ReadingsVal($name,"colormode","") eq "ct" ) { if( ReadingsVal($name,"ct","") =~ m/(\d+) .*/ ) { # calculation from http://www.tannerhelland.com/4435/convert-temperature-rgb-algorithm-code # adjusted by 1000K my $temp = (1000000/$1)/100 + 10; $r = 255; $r = 329.698727446 * ($temp - 60) ** -0.1332047592 if( $temp > 66 ); $r = 0 if( $r < 0 ); $r = 255 if( $r > 255 ); if( $temp <= 66 ) { $g = 99.4708025861 * log($temp) - 161.1195681661; } else { $g = 288.1221695283 * ($temp - 60) ** -0.0755148492; } $g = 0 if( $g < 0 ); $g = 255 if( $g > 255 ); $b = 255; $b = 0 if( $temp <= 19 ); if( $temp < 66 ) { $b = 138.5177312231 * log($temp-10) - 305.0447927307; } $b = 0 if( $b < 0 ); $b = 255 if( $b > 255 ); } } elsif( ReadingsVal($name,"xy","") =~ m/(.+),(.+)/ ) { # calculation from http://www.brucelindbloom.com/index.html my ($x,$y) = ($1, $2); my $Y = ReadingsVal($name,"bri","") / 254.0; #Log 3, "xyY:". $x . " " . $y ." ". $Y; if( $y > 0 ) { my $X = $x * $Y / $y; my $Z = (1 - $x - $y)*$Y / $y; if( $X > 1 || $Y > 1 || $Z > 1 ) { my $f = max($X,max($Y,$Z)); $X /= $f; $Y /= $f; $Z /= $f; } #Log 3, "XYZ: ". $X . " " . $Y ." ". $Y; $r = 0.7982 * $X + 0.3389 * $Y - 0.1371 * $Z; $g = -0.5918 * $X + 1.5512 * $Y + 0.0406 * $Z; $b = 0.0008 * $X + 0.0239 * $Y + 0.9753 * $Z; if( $r > 1 || $g > 1 || $b > 1 ) { my $f = max($r,max($g,$b)); $r /= $f; $g /= $f; $b /= $f; } #Log 3, "rgb: ". $r . " " . $g ." ". $b; $r *= 255; $g *= 255; $b *= 255; } } return sprintf( "%02x%02x%02x", $r+0.5, $g+0.5, $b+0.5 ); } elsif ( $cmd eq "devStateIcon" ) { return 'off' if( ReadingsVal($name,"state","off") eq "off" || ReadingsVal($name,"bri","0") eq 0 ); return ''.$hash->{STATE}.'' if( AttrVal($hash->{NAME}, "model", "") eq "LWL001" ); return '
'. ''.$hash->{STATE}.''. '
' if( ReadingsVal($name,"colormode","") eq "ct" ); return '
'; } return "Unknown argument $cmd, choose one of rgb devStateIcon"; } ################################### # This could be IORead in fhem, But there is none. # Read http://forum.fhem.de/index.php?t=tree&goto=54027&rid=10#msg_54027 # to find out why. sub HUEDevice_ReadFromServer($@) { my ($hash,@a) = @_; my $dev = $hash->{NAME}; no strict "refs"; my $ret; unshift(@a,$dev); $ret = IOWrite($hash, @a); use strict "refs"; return $ret; return if(IsDummy($dev) || IsIgnored($dev)); my $iohash = $hash->{IODev}; if(!$iohash || !$iohash->{TYPE} || !$modules{$iohash->{TYPE}} || !$modules{$iohash->{TYPE}}{ReadFn}) { Log 5, "No I/O device or ReadFn found for $dev"; return; } no strict "refs"; #my $ret; unshift(@a,$dev); $ret = &{$modules{$iohash->{TYPE}}{ReadFn}}($iohash, @a); use strict "refs"; return $ret; } sub HUEDevice_GetUpdate($) { my ($hash) = @_; my $name = $hash->{NAME}; if(!$hash->{LOCAL}) { RemoveInternalTimer($hash); InternalTimer(gettimeofday()+$hash->{INTERVAL}, "HUEDevice_GetUpdate", $hash, 1); } my $result = HUEDevice_ReadFromServer($hash,$hash->{fhem}{id}); if( !defined($result) ) { $hash->{STATE} = "unknown"; return; } elsif( $result->{'error'} ) { $hash->{STATE} = $result->{'error'}->{'description'}; return; } $hash->{modelid} = $result->{'modelid'}; $hash->{name} = $result->{'name'}; $hash->{type} = $result->{'type'}; $hash->{swversion} = $result->{'swversion'}; $attr{$name}{model} = $result->{'modelid'} unless( defined($attr{$name}{model}) || $result->{'modelid'} eq '' ); $attr{$name}{subType} = $hueModels{$attr{$name}{model}}{subType} unless( defined($attr{$name}{subType}) || !defined($attr{$name}{model}) || !defined($hueModels{$attr{$name}{model}}{subType}) ); $attr{$name}{devStateIcon} = '{CommandGet("","'.$name.' devStateIcon")}'; if( !defined( $attr{$name}{webCmd} ) ) { $attr{$name}{webCmd} = 'rgb:rgb ff0000:rgb C8FF12:rgb 0000ff:toggle:on:off' if( $attr{$name}{subType} eq "colordimmer" ); $attr{$name}{webCmd} = 'pct:toggle:on:off' if( $attr{$name}{subType} eq "dimmer" ); $attr{$name}{webCmd} = 'toggle:on:off' if( $attr{$name}{subType} eq "switch" ); } readingsBeginUpdate($hash); my $state = $result->{'state'}; my $on = $state->{on}; my $colormode = $state->{'colormode'}; my $bri = $state->{'bri'}; my $ct = $state->{'ct'}; my $hue = $state->{'hue'}; my $sat = $state->{'sat'}; my $xy = undef; $xy = $state->{'xy'}->[0] .",". $state->{'xy'}->[1] if( defined($state->{'xy'}) ); if( defined($colormode) && $colormode ne $hash->{fhem}{colormode} ) {readingsBulkUpdate($hash,"colormode",$colormode);} if( defined($bri) && $bri != $hash->{fhem}{bri} ) {readingsBulkUpdate($hash,"bri",$bri);} if( defined($ct) && $ct != $hash->{fhem}{ct} ) { if( $ct == 0 ) { readingsBulkUpdate($hash,"ct",$ct); } else { readingsBulkUpdate($hash,"ct",$ct . " (".int(1000000/$ct)."K)"); } } if( defined($hue) && $hue != $hash->{fhem}{hue} ) {readingsBulkUpdate($hash,"hue",$hue);} if( defined($sat) && $sat != $hash->{fhem}{sat} ) {readingsBulkUpdate($hash,"sat",$sat);} if( defined($xy) && $xy ne $hash->{fhem}{xy} ) {readingsBulkUpdate($hash,"xy",$xy);} my $s = ''; if( $on ) { $s = 'on'; if( $on != $hash->{fhem}{on} ) {readingsBulkUpdate($hash,"onoff",1);} my $percent = int( $state->{'bri'} * 100 / 254 ); if( $bri != $hash->{fhem}{bri} ) {readingsBulkUpdate($hash,"level", $percent . ' %');} if( $bri != $hash->{fhem}{bri} ) {readingsBulkUpdate($hash,"pct", $percent);} if( $percent > 0 && $percent < 100 ) { $s = $dim_values{int($percent/7)}; } $s = 'off' if( $percent == 0 ); } else { $s = 'off'; if( $on != $hash->{fhem}{on} ) {readingsBulkUpdate($hash,"onoff",0);} } if( $s ne $hash->{STATE} ) {readingsBulkUpdate($hash,"state",$s);} readingsEndUpdate($hash,defined($hash->{LOCAL} ? 0 : 1)); $hash->{fhem}{on} = $on; $hash->{fhem}{colormode} = $colormode; $hash->{fhem}{bri} = $bri; $hash->{fhem}{ct} = $ct; $hash->{fhem}{hue} = $hue; $hash->{fhem}{sat} = $sat; $hash->{fhem}{xy} = $xy; } 1; =pod =begin html

HUEDevice


=end html =cut