# $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', # 19 getGroups => '1E', # 30 addToGroup => '20', # 32 removeFromGroup => '21', # 33 getGroupInfo => '26', # 38 setGroupName => '27', # 39 setName => '28', # 40 setDim => '31', # 49 setOnOff => '32', # 50 setCT => '33', # 51 setRGB => '36', # 54 setPhysical => '38', goToScene => '52', # 82 getStatus => '68', # 104 setSoftOn => 'DB', # -37 setSoftOff => 'DC', # -36 switch => 0x1, ctdimmer => 0x2, dimmer => 0x4, colordimmer => 0x8, extcolordimmer => 0x10, motiondetector => 0x20, pushbuton => 0x41, }; sub LIGHTIFY_Initialize($) { my ($hash) = @_; $hash->{ReadFn} = "LIGHTIFY_Read"; $hash->{WriteFn} = "LIGHTIFY_Write"; $hash->{Clients} = ":HUEDevice:"; $hash->{DefFn} = "LIGHTIFY_Define"; $hash->{RenameFn} = "LIGHTIFY_Rename"; $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 disabledForIntervals 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; $hash->{INTERVAL} = 60; $hash->{NOTIFYDEV} = "global"; if( $init_done ) { LIGHTIFY_Disconnect($hash); LIGHTIFY_Connect($hash); } elsif( $hash->{STATE} ne "???" ) { readingsSingleUpdate($hash, 'state', 'initialized', 1 ); } $attr{$name}{pollDevices} = 1; return undef; } sub LIGHTIFY_Rename($$$) { my ($new,$old) = @_; foreach my $chash ( values %{$modules{HUEDevice}{defptr}} ) { next if( !$chash->{IODev} ); next if( $chash->{IODev}{NAME} ne $new ); HUEDevice_IODevChanged($chash, $old, $new); } } 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( IsDisabled($name) > 0 ); $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), Timeout => 4, ); if($socket) { readingsSingleUpdate($hash, 'state', 'connected', 1 ); $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, '00', getDevices ." 00 00 00 00 01" ); LIGHTIFY_sendRaw( $hash, '00', getGroups ." 00 00 00 00 00" ); } 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}); readingsSingleUpdate($hash, 'state', 'disconnected', 1 ); 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, $flag, $hex, $force) = @_; my $name = $hash->{NAME}; return undef if( IsDisabled($name) > 0 ); return "not connected" if( !$hash->{CD} ); if( !$force && $hash->{UNCONFIRMED} ) { for(my $i = int(@{$hash->{SEND_QUEUE}}); $i >= 0; --$i) { my $a = $hash->{SEND_QUEUE}[$i]; next if( !$a ); if( $flag eq $a->[0] && $hex eq $a->[1] ) { Log3 $name, 4, "$name: discard: $flag, $hex"; if( 1 ) { splice @{$hash->{SEND_QUEUE}}, $i, 1; } else { return undef; } } } Log3 $name, 4, "$name: enque: $flag, $hex"; push @{$hash->{SEND_QUEUE}}, [$flag, $hex, $hash->{CL}]; 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 ) . $flag . $hex; Log3 $name, 4, "$name: sending: ". $hex; #return undef if( IsDisabled($name) > 0 ); #return "not connected" if( !$hash->{CD} ); syswrite($hash->{CD}, pack('H*', $hex)); $hash->{helper}{CL} = $hash->{CL}; $hash->{UNCONFIRMED}++ if( !$force ); RemoveInternalTimer($hash, "LIGHTIFY_sendNext"); 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 $flag = '00'; my $light = $chash->{ID}; if( $chash->{helper}->{devtype} && $chash->{helper}->{devtype} eq 'G' ) { my $group = $chash->{ID}; $group =~ s/^.//; $group = sprintf( "%02X", $group ); if( $group eq '00' ) { $light = 'FF FF FF FF FF FF FF FF'; } else { $flag = '02'; $light = "$group 00 00 00 00 00 00 00"; if( $obj ) { if( defined($obj->{name}) ) { my $name; for( my $i = 0; $i < 15; ++$i ) { $name .= sprintf( "%02X ", ord(substr($obj->{name},$i,1)) ); } $name .= '00'; LIGHTIFY_sendRaw( $hash, $flag, setGroupName ." 00 00 00 00 $group 00 $name" ); CommandAttr(undef,"$chash->{NAME} alias $obj->{name}"); CommandSave(undef,undef) if( AttrVal( "autocreate", "autosave", 1 ) ); #LIGHTIFY_sendRaw( $hash, '00', getGroups ." 00 00 00 00 00" ); return undef; } } } $chash->{helper}{on} = -1; } if( $obj ) { if( defined($obj->{name}) ) { my $name; for( my $i = 0; $i < 15; ++$i ) { $name .= sprintf( "%02X ", ord(substr($obj->{name},$i,1)) ); } $name .= '00'; LIGHTIFY_sendRaw( $hash, $flag, setName ." 00 00 00 00 $light $name" ); CommandAttr(undef,"$chash->{NAME} alias $obj->{name}"); CommandSave(undef,undef) if( AttrVal( "autocreate", "autosave", 1 ) ); #LIGHTIFY_sendRaw( $hash, '00', getDevices ." 00 00 00 00 01" ); return undef; } } my $force = ($chash->{helper}->{update_timeout} && $chash->{helper}->{update_timeout} == -1); my $transitiontime = 2; my $json = { state => { xreachable => 1, } }; if( $obj ) { if( defined($obj->{on}) ) { my $onoff = "00"; $onoff = "01" if( $obj->{on} ); LIGHTIFY_sendRaw( $hash, $flag, setOnOff ." 00 00 00 00 $light $onoff", $force ) if( 1 || $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 ); $transitiontime = $obj->{transitiontime} if( defined($obj->{transitiontime}) ); my $t = sprintf( '%02X%02X', $transitiontime & 0xff, $transitiontime >> 8 ); LIGHTIFY_sendRaw( $hash, $flag, setCT ." 00 00 00 00 $light $ct $t", $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 ); $transitiontime = $obj->{transitiontime} if( defined($obj->{transitiontime}) ); my $t = sprintf( '%02X%02X', $transitiontime & 0xff, $transitiontime >> 8 ); LIGHTIFY_sendRaw( $hash, $flag, setRGB ." 00 00 00 00 $light $rgb 00 $t", $force ); } if( defined($obj->{bri}) && !defined($obj->{hue}) && !defined($obj->{sat}) ) { my $bri = $obj->{bri}; $bri /= 2.54; $bri = sprintf( "%02X", $bri ); $transitiontime = $obj->{transitiontime} if( defined($obj->{transitiontime}) ); my $t = sprintf( '%02X%02X', $transitiontime & 0xff, $transitiontime >> 8 ); LIGHTIFY_sendRaw( $hash, $flag, setDim ." 00 00 00 00 $light $bri $t", $force ); $json->{state}{bri} = $obj->{bri}; } } my $fake = (0 && $chash->{helper}->{update_timeout} && $chash->{helper}->{update_timeout} != 0); 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 { if( $flag eq '00' && $light ne 'FF FF FF FF FF FF FF FF' ) { LIGHTIFY_sendRaw( $hash, '00', getStatus ." 00 00 00 00 $light" ); $chash->{helper}{transitiontime} = int($transitiontime/10) if( $obj ); #RemoveInternalTimer($chash); #InternalTimer(gettimeofday()+5, "HUEDevice_GetUpdate", $chash, 0); } else { LIGHTIFY_sendRaw( $hash, '00', getDevices ." 00 00 00 00 01" ); } } my %ret = (); return \%ret; } sub LIGHTIFY_Set($$@) { my ($hash, $name, $cmd, @args) = @_; $hash->{".triggerUsed"} = 1; my $list = ""; $list .= "on off " if( $hash->{CD} ); $list .= "raw " if( $hash->{CD} ); $list .= "reconnect:noArg "; $list .= "goToScene " if( $hash->{CD} ); $list .= "setRGBW " if( $hash->{CD} ); $list .= "setSoftOn setSoftOff " if( $hash->{CD} ); $list .= "statusRequest:noArg " if( $hash->{CD} ); if( $cmd eq 'on' ) { LIGHTIFY_sendRaw( $hash, '00', setOnOff ." 00 00 00 00 FF FF FF FF FF FF FF FF 01", 1 ); LIGHTIFY_sendRaw( $hash, '00', getDevices ." 00 00 00 00 01" ); return undef; } elsif( $cmd eq 'off' ) { LIGHTIFY_sendRaw( $hash, '00', setOnOff ." 00 00 00 00 FF FF FF FF FF FF FF FF 00", 1 ); LIGHTIFY_sendRaw( $hash, '00', getDevices ." 00 00 00 00 01" ); return undef; } elsif( $cmd eq 'raw' ) { return LIGHTIFY_sendRaw( '00', $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' ) { delete $hash->{CL}; LIGHTIFY_Disconnect($hash); LIGHTIFY_Connect($hash); return undef; } elsif( $cmd eq 'statusRequest' ) { return LIGHTIFY_sendRaw( $hash, '00', getDevices ." 00 00 00 00 01" ); } elsif( $cmd eq 'getGroups' ) { return LIGHTIFY_sendRaw( $hash, '00', getGroups ." 00 00 00 00 00" ); } elsif( $cmd eq 'getGroupInfo' ) { return "usage: getGroupInfo " if( !$args[0] ); return "usage: musst be numeric" if( $args[0] !~ /^\d*$/ ); return "usage: musst be in the range [0-255]" if( $args[0] < 0 || $args[0] > 255 ); my $group = $args[0]; $group = 1 if( !$group || $group < 1 ); $group = sprintf( "%02i", $group ); return LIGHTIFY_sendRaw( $hash, '00', getGroupInfo ." 00 00 00 00 $group 00" ); } elsif( $cmd eq 'addToGroup' ) { return "usage: addToGroup " if( !$args[2] ); return "usage: musst be numeric" if( $args[0] !~ /^\d*$/ ); return "usage: musst be in the range [0-255]" if( $args[0] < 0 || $args[0] > 255 ); return "usage: musst be a 16 hex digit device address" if( $args[1] !~ /^[A-F0-9]{16}$/i ); my $group = $args[0]; $group = 1 if( !$group || $group < 1 ); $group = sprintf( "%02i", $group ); my $new = join( ' ', @args[2..@args-1]); my $name; for( my $i = 0; $i < 15; ++$i ) { $name .= sprintf( "%02X ", ord(substr($new,$i,1)) ); } my $length = sprintf( "%02X ", length($name) ); LIGHTIFY_sendRaw( $hash, '02', addToGroup ." 00 00 00 00 $group 00 $args[1] $length $name" ); LIGHTIFY_sendRaw( $hash, '00', getGroups ." 00 00 00 00 00" ); return undef; } elsif( $cmd eq 'removeFromGroup' ) { return "usage: removeFromGroup " if( !$args[1] ); return "usage: musst be numeric" if( $args[0] !~ /^\d*$/ ); return "usage: musst be in the range [0-255]" if( $args[0] < 0 || $args[0] > 255 ); return "usage: musst be a 16 hex digit device address" if( $args[1] !~ /^[A-F0-9]{16}$/i ); my $group = $args[0]; $group = 1 if( !$group || $group < 1 ); $group = sprintf( "%02i", $group ); return LIGHTIFY_sendRaw( $hash, '02', removeFromGroup ." 00 00 00 00 $group 00 $args[1]" ); } elsif( $cmd eq 'getStatus' ) { return "usage: getStatus " if( !$args[0] ); return "usage: musst be a 16 hex digit device address" if( $args[0] !~ /^[A-F0-9]{16}$/i ); return LIGHTIFY_sendRaw( $hash, '00', getStatus ." 00 00 00 00 $args[0]" ); } elsif( $cmd eq 'goToScene' ) { return "usage: goToScene " if( !$args[0] ); return "usage: musst be numeric" if( $args[0] !~ /^\d*$/ ); my $scene = $args[0]; $scene = 1 if( !$scene || $scene < 1 ); $scene = sprintf( "%02i", $scene ); LIGHTIFY_sendRaw( $hash, '00', goToScene ." 00 00 00 00 $scene" ); return LIGHTIFY_sendRaw( $hash, '00', getDevices ." 00 00 00 00 01" ); } elsif( $cmd eq 'saveScene' ) { return "usage: saveScene " if( !$args[0] ); return "usage: musst be numeric" if( $args[0] !~ /^\d*$/ ); my $scene = $args[0]; $scene = 1 if( !$scene || $scene < 1 ); $scene = sprintf( "%02i", $scene ); return LIGHTIFY_sendRaw( $hash, '02', goToScene ." 00 00 00 00 $scene" ); return LIGHTIFY_sendRaw( $hash, '00', getDevices ." 00 00 00 00 01" ); } elsif( $cmd eq 'setSoftOn' ) { return "usage: setSoftOn " if( !defined($args[1]) ); return "usage: musst be a 16 hex digit device address" if( $args[0] !~ /^[A-F0-9]{16}$/i ); return "usage: musst be numeric" if( $args[1] !~ /^\d*$/ ); return "usage: musst be in the range [0-255]" if( $args[1] < 0 || $args[1] > 255 ); my $transitiontime = sprintf( '%02X', $args[1] & 0xff ); return LIGHTIFY_sendRaw( $hash, '00', setSoftOn ." 00 00 00 00 $args[0] $transitiontime" ); } elsif( $cmd eq 'setSoftOff' ) { return "usage: setSoftOff " if( !defined($args[1]) ); return "usage: musst be a 16 hex digit device address" if( $args[0] !~ /^[A-F0-9]{16}$/i ); return "usage: musst be numeric" if( $args[1] !~ /^\d*$/ ); return "usage: musst be in the range [0-255]" if( $args[1] < 0 || $args[1] > 255 ); my $transitiontime = sprintf( '%02X', $args[1] & 0xff ); return LIGHTIFY_sendRaw( $hash, '00', setSoftOff ." 00 00 00 00 $args[0] $transitiontime" ); } elsif( $cmd eq 'setPhysical' ) { return "usage: setPhysical " if( !defined($args[0]) ); return "usage: musst be a 16 hex digit device address" if( $args[0] !~ /^[A-F0-9]{16}$/i ); return LIGHTIFY_sendRaw( $hash, '00', setPhysical ." 00 00 00 00 $args[0] 00" ); } elsif( $cmd eq 'setRGBW' ) { return "usage: setRGBW " if( !defined($args[1]) ); return "usage: musst be a 16 hex digit device address" if( $args[0] !~ /^[A-F0-9]{16}$/i ); return "usage: musst be a 8 hex digits rgbw color" if( $args[1] !~ /^[A-F0-9]{8}$/i ); return LIGHTIFY_sendRaw( $hash, '00', setRGB ." 00 00 00 00 $args[0] $args[1] 0200" ); } return "Unknown argument $cmd, choose one of $list"; } sub LIGHTIFY_poll($) { my ($hash) = @_; RemoveInternalTimer($hash, "LIGHTIFY_poll"); LIGHTIFY_sendRaw( $hash, '00', getDevices ." 00 00 00 00 01" ); } 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 $a = shift @{$hash->{SEND_QUEUE}}; if( $a ) { $hash->{CL} = $a->[2]; LIGHTIFY_sendRaw( $hash, $a->[0], $a->[1] ) if( $a ); delete $hash->{CL}; } } } sub LIGHTIFY_toJson($$$$$$$$$$) { my ($hash,$chash,$id,$reachable,$onoff,$dim,$ct,$r,$g,$b) = @_; my $json = { state => { } }; if( $chash ) { $json->{uniqueid} = $id if( defined($id) ); $json->{state}{on} = $onoff if( defined($onoff) ); $json->{state}{reachable} = $reachable? 1 : 0 if( defined($reachable) ); if( !$chash->{helper}{type} ) { Log3 $hash->{NAME}, 2, "$chash->{NAME}: unknown light type"; } elsif( $chash->{helper}{type} == motiondetector ) { $json->{type} = 'MotionDetector'; } elsif( $chash->{helper}{type} == extcolordimmer ) { $json->{type} = 'Extended color light'; } elsif( $chash->{helper}{type} == colordimmer ) { $json->{type} = 'Color light'; } elsif( $chash->{helper}{type} == ctdimmer ) { $json->{type} = 'Color temperature tight'; } elsif( $chash->{helper}{type} == dimmer ) { $json->{type} = 'Dimmable'; } else { $json->{type} = 'On/Off'; } my $has_ct = ($chash->{helper}{type} & 0x02) ? 1: 0; my $has_rgb = ($chash->{helper}{type} & 0x08) ? 1 : 0; my $is_sensor = ($chash->{helper}{type} >= 0x20) ? 1 : 0; if( $is_sensor ) { $json->{state}->{lastupdated} = TimeNow(); if( $chash->{helper}{type} == motiondetector ) { if( $r eq '01' ) { $json->{config}->{on} = 1; $json->{state}->{presence} = $g eq '01'?1:0; } else { $json->{config}->{on} = 0; } } } elsif( $has_rgb ) { if( $has_ct && "$r$g$b" eq '111' ) { $json->{state}->{colormode} = 'ct'; } elsif( defined($r) ) { 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); $json->{state}{colormode} = 'hs'; $json->{state}{hue} = int( $h * 65535 ), $json->{state}{sat} = int( $s * 254 ), $json->{state}{bri} = int( $v * 254 ), } } elsif( $has_ct && $ct ) { $json->{state}->{colormode} = 'ct'; } else { } $json->{state}{ct} = int(1000000/$ct) if( $ct ); $json->{state}{bri} = int($dim/100*254) if( defined($dim) ); } return $json; } 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 $flag = substr($hex,2*2,2*1); my $cmd = substr($hex,2*3,2*1); my $cnt = substr($hex,2*4,2*1); my $err = substr($hex,2*8,2*1); if( $err ne '00' ) { readingsSingleUpdate($hash, 'lastError', "for cmd: $cmd: err: $err", 0 ); Log3 $name, 3, "$name: got error: $err "; return undef; } if( $cmd eq getDevices ) { my $nr_lights = hex(substr($hex,2*10,2*1).substr($hex,2*9,2*1)); return undef if( !$nr_lights ); my $offset = ($length+2-11) / $nr_lights; Log3 $name, 2, "$name: warning: offset for cmd $cmd is $offset instead of 50" if( $offset != 50 ); 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 $firmware = substr($hex,$i*$offset*2+2*22,2*4); my $reachable = hex(substr($hex,$i*$offset*2+2*26,2*1)); my $groups = (substr($hex,$i*$offset*2+2*28,2*1).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 $w = substr($hex,$i*$offset*2+2*36,2*1); my $alias = pack('H*', substr($hex,$i*$offset*2+2*37,2*15)); $alias =~ s/\x00//g; #my $count1 = substr($hex,$i*$offset*2+2*53,2*4); #reportMissingCount #my $count2 = substr($hex,$i*$offset*2+2*57,2*4); #pollingCount #Log 1, "count1: $count1, count2, $count2"; my $has_ct = (hex($type) & 0x02) ? 1: 0; my $has_rgb = (hex($type) & 0x08) ? 1 : 0; my $is_sensor = (hex($type) >= 0x20) ? 1 : 0; #$has_ct = 1 if( $type eq '00' ); Log3 $name, 4, "$alias: $id:$short, type: $type (ct:$has_ct, rgb:$has_rgb, sensor:$is_sensor), firmware: $firmware, reachable: $reachable, groups: $groups, onoff: $onoff, dim: $dim, ct: $ct, rgb: $r$g$b, w: $w"; #my $code = $id; my $code = $name ."-". $id; $code = $name ."-S". $id if( $is_sensor ); if( defined($modules{HUEDevice}{defptr}{$code}) ) { Log3 $name, 5, "$name: id '$id' already defined as '$modules{HUEDevice}{defptr}{$code}->{NAME}'"; } else { my $devname = "LIGHTIFY" . $id; #my $define= "$devname HUEDevice $id"; my $define= "$devname HUEDevice $id IODev=$name"; $define= "$devname HUEDevice sensor $id IODev=$name" if( $is_sensor ); 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} ) { $chash->{helper}{type} = hex($type); $chash->{helper}{type} = extcolordimmer if( !$chash->{helper}{type} ); my $json = LIGHTIFY_toJson($hash, $chash, $id, $reachable, $onoff, $dim, $ct, $r, $g, $b); my $changed = HUEDevice_Parse( $chash, $json ); if( $changed || $chash->{helper}{transitiontime} ) { RemoveInternalTimer($chash); InternalTimer(gettimeofday()+1, "HUEDevice_GetUpdate", $chash, 0); $chash->{helper}{transitiontime} -= 1 if( $chash->{helper}{transitiontime} ); } } } if( $autocreated ) { Log3 $name, 2, "$name: autocreated $autocreated devices"; CommandSave(undef,undef) if( AttrVal( "autocreate", "autosave", 1 ) ); } RemoveInternalTimer($hash, "LIGHTIFY_poll"); InternalTimer(gettimeofday()+$hash->{INTERVAL}, "LIGHTIFY_poll", $hash, 0); } elsif( $cmd eq getGroups ) { my $nr_groups = hex(substr($hex,2*10,2*1).substr($hex,2*9,2*1)); #Log 1, unpack 'v', pack 'H*', substr($hex,2*9,2*2); return undef if( !$nr_groups ); my $offset = ($length+2-11) / $nr_groups; Log3 $name, 2, "$name: warning: offset for cmd $cmd is $offset instead of 18" if( $offset != 18 ); my @groups; my $autocreated = 0; for( my $i = 0; $i <= $nr_groups; ++$i ) { my $id; my $alias; if( $i == 0 ) { $id = 0; $alias = 'Gruppe alles'; } else { $id = hex(substr($hex,($i-1)*$offset*2+2*12,2*1).substr($hex,($i-1)*$offset*2+2*11,2*1)); $alias = pack('H*', substr($hex,($i-1)*$offset*2+2*13,2*15)); $alias =~ s/\x00//g; my $group = sprintf( "%02X", $id ); $hash->{CL} = $hash->{helper}{CL}; LIGHTIFY_sendRaw( $hash, '00', getGroupInfo ." 00 00 00 00 $group 00" ); delete $hash->{CL}; } push @groups, "$id: $alias"; #my $code = $id; my $code = $name ."-G". $id; if( defined($modules{HUEDevice}{defptr}{$code}) ) { Log3 $name, 5, "$name: id '$id' already defined as '$modules{HUEDevice}{defptr}{$code}->{NAME}'"; } else { my $devname = "LIGHTIFYGroup" . $id; my $define= "$devname HUEDevice group $id IODev=$name"; Log3 $name, 4, "$name: create new device '$devname' for group nr. '$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"); $cmdret = CommandAttr(undef,"$devname subType switch") if( $id == 0 ); $autocreated++; } } } Log3 $name, 4, "groups: " .join( ', ', @groups ); if( $autocreated ) { Log3 $name, 2, "$name: autocreated $autocreated groups"; CommandSave(undef,undef) if( AttrVal( "autocreate", "autosave", 1 ) ); } asyncOutput( $hash->{helper}{CL}, "got groups: ". join( ', ', @groups ) ) if( $hash->{helper}{CL} ); } elsif( $cmd eq getGroupInfo ) { my $nr = hex(substr($hex,2*10,2*1).substr($hex,2*9,2*1)); my $alias = pack('H*', substr($hex,2*11,2*15)); my $nr_lights = hex(substr($hex,2*27,2*1)); return undef if( !$nr_lights ); $alias =~ s/\x00//g; my $offset = ($length+2-28) / $nr_lights; # should be 8 Log3 $name, 2, "$name: warning: offset for cmd $cmd is $offset instead of 8" if( $offset != 8 ); my @lights; for( my $i = 0; $i < $nr_lights; ++$i ) { my $light = substr($hex,$i*$offset*2+2*28,2*8); push @lights, $light; } Log3 $name, 4, "group $nr: alias: $alias, lights: " .join( ',', @lights ); my $code = $name ."-G". $nr; if( my $chash = $modules{HUEDevice}{defptr}{$code} ) { $chash->{lights} = join( ',', @lights ); } asyncOutput( $hash->{helper}{CL}, "group info: $nr: $alias, lights: ". join( ', ', @lights ) ) if( $hash->{helper}{CL} ); } elsif( $cmd eq setOnOff ) { my $id = substr($hex,2*11,2*8); my $onoff = hex(substr($hex,2*19,2*1)); } elsif( $cmd eq getStatus ) { my $id = substr($hex,2*11,2*8); my $json; my $code = $name ."-". $id; my $chash = $modules{HUEDevice}{defptr}{$code}; if( !$chash ) { $code = $name ."-S". $id; $chash = $modules{HUEDevice}{defptr}{$code}; } if( $length < 30 ) { $json = { state => { } }; if( substr($hex,2*19,2*1) eq 'FF' ) { Log3 $name, 4, "$id, not reachable"; $json = { state => { reachable => 0 } }; } } else { my $reachable = hex(substr($hex,2*20,2*1)); my $onoff = hex(substr($hex,2*21,2*1)); my $dim = hex(substr($hex,2*22,2*1)); my $ct = hex(substr($hex,2*24,2*1).substr($hex,2*23,2*1)); my $r = substr($hex,2*25,2*1); my $g = substr($hex,2*26,2*1); my $b = substr($hex,2*27,2*1); my $w = substr($hex,2*28,2*1); Log3 $name, 4, "$id, reachable: $reachable, onoff: $onoff, dim: $dim, ct: $ct, rgb: $r$g$b, w: $w"; $json = LIGHTIFY_toJson($hash, $chash, $id, $reachable, $onoff, $dim, $ct, $r, $g, $b); } my $changed = HUEDevice_Parse( $chash, $json ) if( $chash ); if( $changed || $chash->{helper}{transitiontime} ) { RemoveInternalTimer($chash); InternalTimer(gettimeofday()+1, "HUEDevice_GetUpdate", $chash, 0); $chash->{helper}{transitiontime} -= 1 if( $chash->{helper}{transitiontime} ); } } else { Log3 $name, 4, "$name: unhandled message $hex "; } } 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"; $hash->{PARTIAL} .= $hex; my $length = hex(substr($hash->{PARTIAL},2*1,2*1).substr($hash->{PARTIAL},2*0,2*1)); while( $hash->{PARTIAL} && $length+2 <= length($hash->{PARTIAL})/2 ) { $hex = substr($hash->{PARTIAL},0,$length*2+2*2); $hash->{PARTIAL} = substr($hash->{PARTIAL},$length*2+2*2); $length = hex(substr($hash->{PARTIAL},2*1,2*1).substr($hash->{PARTIAL},2*0,2*1)) if( $hash->{PARTIAL} ); LIGHTIFY_Parse($hash, $hex); } readingsSingleUpdate($hash, 'state', $hash->{READINGS}{state}{VAL}, 0); LIGHTIFY_sendNext( $hash ) if( !$hash->{PARTIAL} ); #RemoveInternalTimer($hash); #InternalTimer(gettimeofday()+2, "LIGHTIFY_sendNext", $hash, 0); } 1; =pod =item tag protocol:zigbee =item summary module for the osram lightify gateway =item summary_DE Modul für das Osram LIGHTFY Gateway =begin html

LIGHTIFY

    Module to integrate a OSRAM LIGHTIFY gateway into FHEM;.

    The actual LIGHTIFY lights are defined as HUEDevice devices.

    All newly found devices and groups are autocreated at startup and added to the room LIGHTIFY.

    Notes:
    • Autocreate only works for the first gateway. Devices on other gateways have to be manualy defined.


    Define
      define <name> LIGHTIFY <host>

      Defines a LIGHTIFY gateway device with address <host>.

      Examples:
        define gateway LIGHTIFY 10.0.1.100

    Get

    Set
    • on
    • off
    • goToScene <sceneId>
    • setSoftOn <addr> <transitiontime>
    • setSoftOff <addr> <transitiontime>
    • reconnect
      Closes and reopens the connection to the gateway.
    • statusRequest
      Update light status.

    Attributes

=end html =cut