# $Id$ package main; use strict; use warnings; use Color; use JSON; use IO::Socket::INET; use IO::File; use IO::Handle; use Data::Dumper; use constant { getDevices => '13', setDim => '31', setOnOff => '32', setCT => '33', setRGB => '36', }; sub LIGHTIFY_Initialize($) { my ($hash) = @_; $hash->{ReadFn} = "LIGHTIFY_Read"; $hash->{WriteFn} = "LIGHTIFY_Write"; $hash->{Clients} = ":HUEDevice:"; $hash->{DefFn} = "LIGHTIFY_Define"; $hash->{NOTIFYDEV} = "global"; $hash->{NotifyFn} = "LIGHTIFY_Notify"; $hash->{UndefFn} = "LIGHTIFY_Undefine"; $hash->{SetFn} = "LIGHTIFY_Set"; #$hash->{GetFn} = "LIGHTIFY_Get"; $hash->{AttrFn} = "LIGHTIFY_Attr"; $hash->{AttrList} = "disable:1,0 pollDevices:1"; } ##################################### sub LIGHTIFY_Define($$) { my ($hash, $def) = @_; my @a = split("[ \t][ \t]*", $def); return "Usage: define LIGHTIFY host" if(@a < 3); my $name = $a[0]; my $host = $a[2]; $hash->{NAME} = $name; $hash->{Host} = $host; if( $init_done ) { LIGHTIFY_Disconnect($hash); LIGHTIFY_Connect($hash); } elsif( $hash->{STATE} ne "???" ) { $hash->{STATE} = "Initialized"; } $attr{$name}{pollDevices} = 1; return undef; } sub LIGHTIFY_Notify($$) { my ($hash,$dev) = @_; return if($dev->{NAME} ne "global"); return if(!grep(m/^INITIALIZED|REREADCFG$/, @{$dev->{CHANGED}})); LIGHTIFY_Connect($hash); return undef; } sub LIGHTIFY_Connect($) { my ($hash) = @_; my $name = $hash->{NAME}; return undef if( AttrVal($name, "disable", 0 ) == 1 ); $hash->{MSG_NR} = 0; my @send_queue = (); $hash->{SEND_QUEUE} = \@send_queue; $hash->{UNCONFIRMED} = 0; $hash->{PARTIAL} = ""; my $socket = IO::Socket::INET->new( PeerAddr => $hash->{Host}, PeerPort => 4000, #AttrVal($name, "port", 4000) ); if($socket) { $hash->{STATE} = "Connected"; $hash->{LAST_CONNECT} = FmtDateTime( gettimeofday() ); $hash->{FD} = $socket->fileno(); $hash->{CD} = $socket; # sysread / close won't work on fileno $hash->{CONNECTS}++; $selectlist{$name} = $hash; Log3 $name, 3, "$name: connected to $hash->{Host}"; LIGHTIFY_sendRaw( $hash, getDevices ." 00 00 00 00 01" ); } else { Log3 $name, 3, "$name: failed to connect to $hash->{Host}"; LIGHTIFY_Disconnect($hash); InternalTimer(gettimeofday()+10, "LIGHTIFY_Connect", $hash, 0); } } sub LIGHTIFY_Disconnect($) { my ($hash) = @_; my $name = $hash->{NAME}; RemoveInternalTimer($hash); return if( !$hash->{CD} ); close($hash->{CD}) if($hash->{CD}); delete($hash->{FD}); delete($hash->{CD}); delete($selectlist{$name}); $hash->{STATE} = "Disconnected"; Log3 $name, 3, "$name: Disconnected"; $hash->{LAST_DISCONNECT} = FmtDateTime( gettimeofday() ); } sub LIGHTIFY_Undefine($$) { my ($hash, $arg) = @_; LIGHTIFY_Disconnect($hash); return undef; } sub LIGHTIFY_sendRaw($$;$) { my ($hash, $hex, $force) = @_; my $name = $hash->{NAME}; return undef if( AttrVal($name, "disable", 0 ) == 1 ); return "not connected" if( !$hash->{CD} ); if( !$force && $hash->{UNCONFIRMED} ) { foreach my $cmd (@{$hash->{SEND_QUEUE}}) { if( $hex eq $cmd ) { Log3 $name, 4, "$name: discard:". $hex; return undef if( $hex eq $cmd ); } } Log3 $name, 4, "$name: enque:". $hex; push @{$hash->{SEND_QUEUE}}, $hex; return undef; } substr($hex,2*1,2+1,sprintf( '%02x', $hash->{MSG_NR} ) ); $hash->{MSG_NR}++; $hash->{MSG_NR} &= 0xFF; $hex =~ s/ //g; my $length = length($hex)/2+1; $hex = sprintf( '%02x%02x', $length & 0xff, $length >> 8 ) .'00'. $hex; Log3 $name, 4, "$name: sending:". $hex; #return undef if( AttrVal($name, "disable", 0 ) == 1 ); #return "not connected" if( !$hash->{CD} ); syswrite($hash->{CD}, pack('H*', $hex)); $hash->{UNCONFIRMED}++ if( !$force ); RemoveInternalTimer($hash); InternalTimer(gettimeofday()+1, "LIGHTIFY_sendNext", $hash, 0); return undef; } sub LIGHTIFY_Write($@) { my ($hash,$chash,$name,$id,$obj)= @_; #Log 3, Dumper $obj; return undef if( !$chash ); my $fake = $chash->{helper}->{update_timeout} != 0; my $force = $chash->{helper}->{update_timeout} == -1; my $json = { state => { reachable => 1, } }; if( $obj ) { if( defined($obj->{on}) ) { my $onoff = "00"; $onoff = "01" if( $obj->{on} ); LIGHTIFY_sendRaw( $hash, setOnOff ." 00 00 00 00 $chash->{ID} $onoff", $force ) if( $obj->{on} != $chash->{helper}{on} ); $json->{state}{on} = $obj->{on} ? JSON::true : JSON::false; } if( defined($obj->{ct}) ) { my $ct = int(1000000 / $obj->{ct}); $ct = sprintf( '%02x%02x', $ct & 0xff, $ct >> 8 ); my $transitiontime = 2; $transitiontime = $obj->{transitiontime} if( defined($obj->{transitiontime}) ); $transitiontime = sprintf( '%02x%02x', $transitiontime & 0xff, $transitiontime >> 8 ); LIGHTIFY_sendRaw( $hash, setCT ." 00 00 00 00 $chash->{ID} $ct $transitiontime", $force ); $json->{state}{colormode} = 'ct'; $json->{state}{ct} = $obj->{ct}; } elsif( defined($obj->{hue}) || defined($obj->{sat}) ) { my $hue = ReadingsVal($chash->{NAME}, 'hue', 65535 ); my $sat = ReadingsVal($chash->{NAME}, 'sat', 254 ); my $bri = ReadingsVal($chash->{NAME}, 'bri', 254 ); $hue = $obj->{hue} if( defined($obj->{hue}) ); $sat = $obj->{sat} if( defined($obj->{sat}) ); $bri = $obj->{bri} if( defined($obj->{bri}) ); $json->{state}{colormode} = 'hs'; $json->{state}{bri} = $obj->{bri}; $json->{state}{hue} = $obj->{hue}; $json->{state}{sat} = $obj->{sat}; my $h = $hue / 65535.0; my $s = $sat / 254.0; my $v = $bri / 254.0; my ($r,$g,$b) = Color::hsv2rgb($h,$s,$v); $r *= 255; $g *= 255; $b *= 255; my $rgb = sprintf( "%02x%02x%02x", $r+0.5, $g+0.5, $b+0.5 ); my $transitiontime = 2; $transitiontime = $obj->{transitiontime} if( defined($obj->{transitiontime}) ); $transitiontime = sprintf( '%02x%02x', $transitiontime & 0xff, $transitiontime >> 8 ); LIGHTIFY_sendRaw( $hash, setRGB ." 00 00 00 00 $chash->{ID} $rgb ff $transitiontime", $force ); } if( defined($obj->{bri}) && !defined($obj->{hue}) && !defined($obj->{sat}) ) { my $bri = $obj->{bri}; $bri /= 2.54; $bri = sprintf( "%02x", $bri ); my $transitiontime = 2; $transitiontime = $obj->{transitiontime} if( defined($obj->{transitiontime}) ); $transitiontime = sprintf( '%02x%02x', $transitiontime & 0xff, $transitiontime >> 8 ); LIGHTIFY_sendRaw( $hash, setDim ." 00 00 00 00 $chash->{ID} $bri $transitiontime", $force ); $json->{state}{bri} = $obj->{bri}; } } if( $obj && $fake ) { HUEDevice_Parse( $chash, $json ); $chash->{helper}->{update_timeout} = AttrVal($name, "delayedUpdate", 0);; #$chash->{helper}->{update_timeout} = 1 if( !$chash->{helper}->{update_timeout} ); RemoveInternalTimer($chash); InternalTimer(gettimeofday()+$chash->{helper}->{update_timeout}, "HUEDevice_GetUpdate", $chash, 0); } else { LIGHTIFY_sendRaw( $hash, getDevices ." 00 00 00 00 01" ); } return undef; } sub LIGHTIFY_Set($$@) { my ($hash, $name, $cmd, @args) = @_; my $list = ""; $list .= "raw " if( $hash->{CD} ); $list .= "reconnect:noArg "; $list .= "statusRequest:noArg " if( $hash->{CD} ); if( $cmd eq 'raw' ) { return LIGHTIFY_sendRaw( $hash, join( '', @args ) ); return undef; } elsif( $cmd eq 'received' ) { my $hex = join( '', @args ); $hex =~ s/ //g; LIGHTIFY_Parse($hash, $hex); return undef; } elsif( $cmd eq 'reconnect' ) { LIGHTIFY_Disconnect($hash); LIGHTIFY_Connect($hash); return undef; } elsif( $cmd eq 'statusRequest' ) { return LIGHTIFY_sendRaw( $hash, getDevices ." 00 00 00 00 01" ); return undef; } return "Unknown argument $cmd, choose one of $list"; } sub LIGHTIFY_poll($) { my ($hash) = @_; RemoveInternalTimer($hash); InternalTimer(gettimeofday()+$hash->{INTERVAL}, "LIGHTIFY_poll", $hash, 0); } sub LIGHTIFY_Get($$@) { my ($hash, $name, $cmd) = @_; my $list = ""; return "Unknown argument $cmd, choose one of $list"; } sub LIGHTIFY_Attr($$$) { my ($cmd, $name, $attrName, $attrVal) = @_; my $orig = $attrVal; $attrVal = int($attrVal) if($attrName eq "interval"); $attrVal = 60 if($attrName eq "interval" && $attrVal < 60 && $attrVal != 0); if( $attrName eq "disable" ) { my $hash = $defs{$name}; if( $cmd eq "set" && $attrVal ne "0" ) { LIGHTIFY_Disconnect($hash); } else { $attr{$name}{$attrName} = 0; LIGHTIFY_Disconnect($hash); LIGHTIFY_Connect($hash); } } if( $cmd eq "set" ) { if( $orig ne $attrVal ) { $attr{$name}{$attrName} = $attrVal; return $attrName ." set to ". $attrVal; } } return; } sub LIGHTIFY_sendNext($) { my ($hash) = @_; $hash->{UNCONFIRMED}-- if( $hash->{UNCONFIRMED} > 0 ); if( $hash->{SEND_QUEUE} ) { my $hex = shift @{$hash->{SEND_QUEUE}}; LIGHTIFY_sendRaw( $hash, $hex ) if( $hex ); } } sub LIGHTIFY_Parse($$) { my ($hash,$hex) = @_; my $name = $hash->{NAME}; $hex = uc($hex); Log3 $name, 4, "$name: parsing: $hex"; my $length = hex(substr($hex,2*1,2*1).substr($hex,2*0,2*1)); my $response = substr($hex,2*3,2*1); my $cnt = substr($hex,2*4,2*1); if( $response eq getDevices ) { my $size = length($hex)/2; my $nr_lights = hex(substr($hex,2*9,2*1)); my $offset = ($size-11) / $nr_lights; my $autocreated = 0; for( my $i = 0; $i < $nr_lights; ++$i ) { my $short = substr($hex,$i*$offset*2+2*11,2*2); my $id = substr($hex,$i*$offset*2+2*13,2*8); my $type = substr($hex,$i*$offset*2+2*21,2*1); my $mode = substr($hex,$i*$offset*2+2*27,2*1); my $onoff = hex(substr($hex,$i*$offset*2+2*29,2*1)); my $dim = hex(substr($hex,$i*$offset*2+2*30,2*1)); my $ct = hex(substr($hex,$i*$offset*2+2*32,2*1).substr($hex,$i*$offset*2+2*31,2*1)); my $r = (substr($hex,$i*$offset*2+2*33,2*1)); my $g = (substr($hex,$i*$offset*2+2*34,2*1)); my $b = (substr($hex,$i*$offset*2+2*35,2*1)); my $alias = pack('H*', substr($hex,$i*$offset*2+2*37,2*16)); my $has_w = (hex($type) & 0x02) ? 1: 0; my $has_rgb = (hex($type) & 0x08) ? 1 : 0; $has_w = 1 if( $type eq '00' ); Log 3, "$alias: $id:$short, type: $type (w:$has_w, rgb:$has_rgb), onoff: $onoff, mode?: $mode dim: $dim, ct: $ct, rgb: $r$g$b"; #my $code = $id; my $code = $name ."-". $id; if( defined($modules{HUEDevice}{defptr}{$code}) ) { Log3 $name, 5, "$name: id '$id' already defined as '$modules{HUEDevice}{defptr}{$code}->{NAME}'"; } else { my $devname = "HUEDevice" . $id; #my $define= "$devname HUEDevice $id"; my $define= "$devname HUEDevice $id IODev=$name"; Log3 $name, 4, "$name: create new device '$devname' for address '$id'"; my $cmdret= CommandDefine(undef,$define); if($cmdret) { Log3 $name, 1, "$name: Autocreate: An error occurred while creating device for id '$id': $cmdret"; } else { $cmdret= CommandAttr(undef,"$devname alias ".$alias); $cmdret= CommandAttr(undef,"$devname room LIGHTIFY"); $cmdret= CommandAttr(undef,"$devname IODev $name"); $autocreated++; } } if( my $chash = $modules{HUEDevice}{defptr}{$code} ) { my( $r, $g, $b ) = (hex($r)/255.0, hex($g)/255.0, hex($b)/255.0); my( $h, $s, $v ) = Color::rgb2hsv($r,$g,$b); my $json = { state => { reachable => ($short eq 'FFFF') ? 0 : 1, on => $onoff, } }; if( $has_rgb && $has_w ) { $json->{type} = 'Extended color light'; } elsif( $has_rgb ) { $json->{type} = 'Color light'; } elsif( $has_w ) { $json->{type} = 'Color Temperature Light'; } if( !$has_rgb ) { $json->{state}->{colormode} = 'ct'; } elsif( $has_w && "$r$g$b" eq '111' ) { $json->{state}->{colormode} = 'ct'; } else { $json->{state}->{colormode} = 'hs'; $json->{state}->{hue} = int( $h * 65535 ), $json->{state}->{sat} = int( $s * 254 ), $json->{state}->{bri} = int( $v * 254 ), } $json->{state}->{ct} = int(1000000/$ct) if( $ct ); $json->{state}->{bri} = int($dim/100*254); HUEDevice_Parse( $chash, $json ); } } return "created $autocreated devices"; } elsif( $response eq setOnOff ) { my $id = substr($hex,2*11,2*8); my $onoff = hex(substr($hex,2*19,2*1)); } LIGHTIFY_sendNext( $hash ); } sub LIGHTIFY_Read($) { my ($hash) = @_; my $name = $hash->{NAME}; my $buf; my $ret = sysread($hash->{CD}, $buf, 1024); if( !defined($ret) || !$ret ) { Log3 $name, 4, "$name: disconnected"; LIGHTIFY_Disconnect($hash); InternalTimer(gettimeofday()+10, "LIGHTIFY_Connect", $hash, 0); return; } my $hex = unpack('H*', $buf); Log3 $name, 5, "$name: received: $hex"; LIGHTIFY_Parse($hash, $hex); return undef; $hash->{PARTIAL} .= $hex; my $length = hex(substr($hash->{PARTIAL},2*1,2*1).substr($hash->{PARTIAL},2*0,2*1)); while( $length*2 <= length($hash->{PARTIAL}) ) { $hex = substr($hash->{PARTIAL},0,$length*2); $hash->{PARTIAL} = substr($hash->{PARTIAL},$length*2); $length = hex(substr($hash->{PARTIAL},2*1,2*1).substr($hash->{PARTIAL},2*0,2*1)); LIGHTIFY_Parse($hash, $hex); } } 1; =pod =begin html

LIGHTIFY


=end html =cut