############################################## # $Id: 32_YeeLightBridge.pm 2017-07-30 thaliondrambor $ # # versions # 00 start # 01 added timeout, keepAlive # 02 added Log at ReadFn # 03 search finally works, added attribut userScene[0-9] # 04 fixed bug, unable to set keepAlive attribut to 0 # # verbose level # 0: quit # 1: error # 2: warning # 3: user command # 4: 1st technical level (detailed internal reporting) # 5: 2nd technical level (full internal reporting) package main; use strict; use warnings; use POSIX; use IO::Socket::Multicast; use IO::Socket::INET; #----------------------------Constants------------------------------------------ use constant GROUP => '239.255.255.250'; # Multicast Group use constant MPORT => 1982; # Multicast Port use constant LPORT => 13254; # Local Port sub YeeLightBridge_Initialize { my ($hash) = @_; $hash->{DefFn} = "YeeLightBridge_Define"; $hash->{UndefFn} = "YeeLightBridge_Undef"; $hash->{ShutdownFn} = "YeeLightBridge_Undef"; $hash->{ReadFn} = "YeeLightBridge_Read"; $hash->{NotifyFn} = "YeeLightBridge_Notify"; $hash->{ParseFn} = "YeeLightBridge_Incoming"; $hash->{SetFn} = "YeeLightBridge_Set"; $hash->{AttrFn} = "YeeLightBridge_Attr"; $hash->{AttrList} = "" ."defaultramp " ."updateIP:0,1 " ."timeout " ."keepAlive " ."userScene[0-9] " ."searchtime " ."$readingFnAttributes"; # Comm with Devices $hash->{Clients} = "YeeLight"; $hash->{MatchList} = {"1:YeeLight" => "^.*"}; $hash->{Write} = "YeeLightBridge_Write"; return undef; } sub YeeLightBridge_Define { my ($hash, $def) = @_; my @a = split("[ \t][ \t]*", $def); my $name = $a[0]; return "$name: Only one bridge for YeeLight can be defined." if ($modules{YeeLightBridge}{defptr}); $hash->{NAME} = $name; $hash->{MULTICAST_GROUP} = GROUP; $hash->{MULTICAST_PORT} = MPORT; $hash->{DeviceName} = GROUP.":".MPORT; $attr{$name}{room} = "YeeLight" if(!defined( $attr{$name}{room})); YeeLightBridgeMulticast_Close($hash); YeeLightBridgeMulticast_Send($hash); $modules{YeeLightBridge}{defptr} = $hash; return undef; } sub YeeLightBridge_Undef { my ($hash) = @_; RemoveInternalTimer($hash); YeeLightBridgeMulticast_Close($hash); delete($modules{YeeLightBridge}{defptr}); return undef; } sub YeeLightBridge_Set { my ($hash, $name, $cmd, @val) = @_; my $list = ""; $list .= "search:noArg "; if (lc $cmd eq 'search') { return YeeLightBridgeMulticast_Send($hash); } return "Unknown argument $cmd, bearword as argument or wrong parameter(s), choose one of $list"; } sub YeeLightBridge_Attr { my ($cmd,$name,$attrName,$attrVal) = @_; if ($cmd eq "set") { if ($attrName eq "defaultramp") { return "Invalid parameter for $attrName. $attrName must be numeric and at least 30." if ($attrVal !~ /^\d?.?\d+$/) || ($attrVal < 30); } elsif ($attrName eq "updateIP") { return "Invalid parameter for $attrName. Choose \"0\" (don't update IP) or \"1\" (update IP)." if ($attrVal != 0) && ($attrVal != 1); } elsif ($attrName eq "timeout") { return "Invalid parameter for $attrName. $attrName must be numeric." if ($attrVal !~ /^\d?.?\d+$/); } elsif ($attrName eq "keepAlive") { return "Invalid parameter for $attrName. $attrName must be numeric and at least 60 or 0." if ($attrVal !~ /^\d?.?\d+$/) || (($attrVal < 60) && ($attrVal != 0)); } elsif ($attrName =~ /userScene[0-9]/) { } elsif ($attrName eq "searchtime") { return "Invalid parameter for $attrName. $attrName must be numeric and greater than 0." if ($attrVal !~ /^\d?.?\d+$/) || ($attrVal < 1); } } return undef; } sub YeeLightBridge_Read { my ($hash) = @_; my $name = $hash->{NAME}; my $sock = $hash->{helper}->{sock}; my $buf; $sock->recv($buf,1024); Log3 $name, 4, "received multicast message on 239.255.255.250:1982:\r\n$buf"; Dispatch($hash,$buf,0); } sub YeeLightBridge_Notify { my ($hash) = @_; my $name = $hash->{NAME}; return undef; } sub YeeLightBridgeMulticast_Create { my ($hash) = @_; my $name = $hash->{NAME}; YeeLightBridgeMulticast_Close($hash); my $mcast = IO::Socket::Multicast->new(Proto => 'udp', LocalPort => MPORT, ReuseAddr => 1) or return "Can't bind mcast: $@\n"; Log3 $name, 4, "$name: opened socket for multicast messages"; $mcast->mcast_add(GROUP) or return "Couldn't set group: $!\n"; $hash->{STATE} = "CONNECTED"; $hash->{helper}->{sock} = $mcast; $hash->{FD} = $mcast->fileno(); $selectlist{$name} = $hash; return undef; } sub YeeLightBridgeMulticast_Send { my ($hash) = @_; my $name = $hash->{NAME}; my $dev = $hash->{DeviceName}; YeeLightBridgeMulticast_Close($hash); my $sock = IO::Socket::Multicast->new() or return "Can't bind socket: $@\n"; $sock->mcast_ttl(3); Log3 $name, 4, "$name: opened socket for search message"; $hash->{STATE} = "SEARCHING"; $hash->{helper}->{sock} = $sock; $hash->{FD} = $sock->fileno(); $selectlist{$name} = $hash; my $send = qq(M-SEARCH * HTTP/1.1\r\nHOST: 239.255.255.250:1982\r\nMAN: "ssdp:discover"\r\nST: wifi_bulb\r\n); $sock->mcast_send($send,$dev) or return "Couldn't send message: $!"; Log3 $name, 4, "$name: Search with $send"; my $searchTime = 3; $searchTime = $attr{$name}{searchtime} if defined($attr{$name}{searchtime}); InternalTimer(gettimeofday() + $searchTime,"YeeLightBridgeMulticast_Create",$hash); return undef; } sub YeeLightBridgeMulticast_Close { my ($hash) = @_; my $name = $hash->{NAME}; if (defined($hash->{helper}->{sock})) { $hash->{helper}->{sock}->close(); delete($hash->{helper}->{sock}); delete($selectlist{$name}); delete($hash->{FD}); Log3 $name, 4, "$name: closing socket."; } $hash->{STATE} = "DISCONNECTED"; return undef; } sub YeeLightBridge_Write { my ($hash,@args) = @_; my $name = $hash->{NAME}; return undef; } 1;