diff --git a/FHEM/71_XiaomiSmartHome.pm b/FHEM/71_XiaomiSmartHome.pm index 8b93362..dfe5455 100644 --- a/FHEM/71_XiaomiSmartHome.pm +++ b/FHEM/71_XiaomiSmartHome.pm @@ -23,20 +23,34 @@ package main; + use strict; use warnings; -use JSON qw( decode_json ); use Data::Dumper; -use IO::Socket; -use IO::Socket::Multicast; -use Crypt::CBC; + +eval "use HTTP::Request::Common"; +return "\nERROR: Please install HTTP::Request::Common" if($@); +eval "use JSON qw( decode_json )"; +return "\nERROR: Please install JSON qw( decode_json )" if($@); +eval "use IO::Socket"; +return "\nERROR: Please install IO::Socket" if($@); +eval "use IO::Socket::Multicast"; +return "\nERROR: Please install IO::Socket::Multicast" if($@); +eval "use Crypt::CBC"; +return "\nERROR: Please install Crypt::CBC" if($@); +eval "use Net::Ping"; +return "\nERROR: Please install Net::Ping" if($@); + use Color; use SetExtensions; + + sub XiaomiSmartHome_Notify($$); sub XiaomiSmartHome_updateSingleReading($$); +sub XiaomiSmartHome_updateAllReadings($); my $iv="\x17\x99\x6d\x09\x3d\x28\xdd\xb3\xba\x69\x5a\x2e\x6f\x58\x56\x2e"; -my $version = "0.10"; +my $version = "0.18"; my %XiaomiSmartHome_gets = ( "getDevices" => ["get_id_list", '^.+get_id_list_ack' ], @@ -45,7 +59,7 @@ my %XiaomiSmartHome_gets = ( my %sets = ( "password" => 1, "rgb:colorpicker,RGB" => 1, - #"pct:colorpicker,BRI,0,300,1300" =>1, + "pct:colorpicker,BRI,0,1,100" => 1, "off" => 0, "on" => 0, ); @@ -66,22 +80,23 @@ sub XiaomiSmartHome_Initialize($) { $hash->{ReadFn} = 'XiaomiSmartHome_Read'; $hash->{WriteFn} = "XiaomiSmartHome_Write"; $hash->{AttrList} = "disable:1,0 " . - "Room " . $readingFnAttributes; $hash->{MatchList} = { "1:XiaomiSmartHome_Device" => "^.+magnet", "2:XiaomiSmartHome_Device" => "^.+motion", "3:XiaomiSmartHome_Device" => "^.+sensor_ht", "4:XiaomiSmartHome_Device" => "^.+switch", - "5:XiaomiSmartHome_Device" => "^.+cube", - "6:XiaomiSmartHome_Device" => "^.+plug"}; + "5:XiaomiSmartHome_Device" => "^.+cube", + "6:XiaomiSmartHome_Device" => "^.+plug", + "7:XiaomiSmartHome_Device" => "^.+86sw2"}; FHEM_colorpickerInit(); -} +} ##################################### sub XiaomiSmartHome_Read($) { my ($hash) = @_; my $name = $hash->{NAME}; + Log3 $name, 5, "$name: Read> Read start"; my $buf = ""; my $ret = sysread($hash->{CD}, $buf, 1024); if (!defined($ret) || $ret <= 0) @@ -94,27 +109,49 @@ sub XiaomiSmartHome_Read($) { my $json = $hash->{helper}{JSON}->incr_parse($buf); my $decoded = decode_json($buf); + if ( ! $decoded ) { + Log3 $name, 4, "$name: Read> Error no JSON Data " . $buf; + return; + } if ($json) { - Log3 $name, 5, "$name> Read:" . $buf; - readingsBeginUpdate( $hash ); - if ($decoded->{'model'} eq 'gateway'){ - if ($decoded->{'cmd'} eq 'report'){ - my $data = decode_json($decoded->{data}); - if (defined $data->{rgb}){ - Log3 $name, 4, "$name>" . " SID: " . $decoded->{'sid'} . " Type: Gateway" . " RGB: " . $data->{rgb} ; - readingsBulkUpdate($hash, "RGB", $data->{rgb} , 1 ); + Log3 $name, 5, "$name: Read> " . $buf; + readingsBeginUpdate( $hash ); + if ($decoded->{'cmd'} eq 'read_ack' || $decoded->{'cmd'} eq 'report' && $decoded->{'model'} eq 'gateway' || $decoded->{'cmd'} eq 'heartbeat' && $decoded->{'model'} eq 'gateway' || $decoded->{'cmd'} eq 'write_ack'){ + if ($decoded->{'model'} && $decoded->{'model'} eq 'gateway' ){ + if ($decoded->{'cmd'} eq 'report'){ + my $data = decode_json($decoded->{data}); + if (defined $data->{rgb}){ + Log3 $name, 4, "$name: Read>" . " SID: " . $decoded->{'sid'} . " Type: Gateway" . " RGB: " . $data->{rgb} ; + readingsBulkUpdate($hash, "RGB", $data->{rgb} , 1 ); + } + } + elsif ($decoded->{'cmd'} eq 'heartbeat'){ + my $data = decode_json($decoded->{data}); + if ($data->{ip} eq $hash->{GATEWAY_IP}){ + readingsBulkUpdate($hash, 'heartbeat', $decoded->{'sid'}, 1 ); + readingsBulkUpdate($hash, 'token', $decoded->{'token'}, 1 ); } + else { + Log3 $name, 4, "$name: Read> IP-Heartbeat Data didnt match! $data->{ip} " . $hash->{GATEWAY_IP} ; + } + } } - elsif ($decoded->{'cmd'} eq 'heartbeat'){ - readingsBulkUpdate($hash, 'HEARTBEAT', $decoded->{'sid'}, 1 ); - readingsBulkUpdate($hash, 'token', $decoded->{'token'}, 1 ); + if ($decoded->{'cmd'} eq 'write_ack'){ + Log3 $name, 4, "$name: Read> Write answer " . $hash->{GATEWAY} ; + my $data = decode_json($decoded->{data}); + if ($data->{error}){ + readingsBulkUpdate($hash, 'heartbeat', $data->{error}, 1 ); + } + else { + readingsBulkUpdate($hash, 'heartbeat', "Write_OK", 1 ); + } } } else { - Log3 $name, 4, "$name> Dispatch! " . $buf; + Log3 $name, 4, "$name: Read> dispatch " . $buf; Dispatch($hash, $buf, undef); - } + } readingsEndUpdate( $hash, 1 ); } } @@ -134,30 +171,82 @@ sub XiaomiSmartHome_getLocalIP(){ } ##################################### +sub XiaomiSmartHome_getGatewaySID($){ + my ($hash, $def) = @_; + my $name = $hash->{NAME}; + my $json; + my $decoded; + my $timeout = $hash->{TIMEOUT} ? $hash->{TIMEOUT} : 3; + my $sidsock = IO::Socket::Multicast->new( Proto => 'udp',LocalAddr => $hash->{FHEMIP}, LocalPort =>'4321', ReuseAddr => 1, Timeout => $timeout) or die "Creating socket: $!\n"; + $sidsock->setsockopt(SOL_SOCKET, SO_RCVTIMEO, pack('l!l!', 30, 0)) or die "setsockopt: $!"; + if ($sidsock){ + my $msg = '{"cmd":"whois"}'; + $sidsock->mcast_send($msg,'224.0.0.50:4321') or die "send: $!"; + eval { + $sidsock->recv($msg, 1024) or die "recv: $!"; + $json = $hash->{helper}{JSON}->incr_parse($msg); + $decoded = decode_json($msg); + $sidsock->close(); + }; + if ($@) { + Log3 $name, 1, "$name: getGatewaySID> Error $@\n"; + $hash->{STATE} = "Disconnected"; + XiaomiSmartHome_disconnect($hash); + return undef; + } + } + if ($json) + { + if ($decoded->{'sid'} ne '') + { + Log3 $name, 3, "$name: Find SID for Gateway: $decoded->{'sid'}"; + return $decoded->{'sid'}; + # XiaomiSmartHome_connect($hash) if( $init_done); + + } + } + else { + Log3 $name, 5, "$name: Did not find a SID for Gateway disconnecting"; + $hash->{STATE} = "Disconnected"; + XiaomiSmartHome_disconnect($hash); + } +} +##################################### + sub XiaomiSmartHome_Define($$) { my ($hash, $def) = @_; my @param = split('[ \t]+', $def); - + my $name = $hash->{NAME}; + if(int(@param) < 3) { return "too few parameters: define XiaomiSmartHome "; } + my $p = Net::Ping->new(); + if ( ! $p->ping($param[2])){ + $hash->{STATE} = "Disconnected"; + XiaomiSmartHome_disconnect($hash); + Log3 $name, 5, "$name: Ping ERROR Gateway disconnecting"; + $p->close(); + } my $definition = $param[2]; - $hash->{DEF} = $definition; $hash->{NOTIFYDEV} = "global"; $hash->{NAME} = $param[0]; $hash->{VERSION} = $version; - $hash->{GATEYWAY} = $param[2]; + $hash->{GATEWAY} = $param[2]; + $hash->{GATEWAY_IP} = $param[2]; + $hash->{helper}{JSON} = JSON->new->utf8(); $hash->{FHEMIP} = XiaomiSmartHome_getLocalIP(); $hash->{STATE} = "initialized"; - $hash->{helper}{host} = $definition; - $hash->{helper}{JSON} = JSON->new->utf8(); - Log3 $hash->{NAME}, 5, "$hash->{NAME}> $definition"; - $attr{$hash->{NAME}}{webCmd} = "rgb:rgb ff0000:rgb 00ff00:rgb 0000ff:on:off"; + $hash->{SID} = XiaomiSmartHome_getGatewaySID($hash); + $hash->{helper}{host} = $definition; + Log3 $name, 5, "$name: $definition"; # Define devStateIcon $attr{$hash->{NAME}}{devStateIcon} = '{Color_devStateIcon(ReadingsVal($name,"rgb","000000"))}' if(!defined($attr{$hash->{NAME}}{devStateIcon})); + $attr{$hash->{NAME}}{room} = "MiSmartHome" if( !defined( $attr{$hash->{NAME}}{room} ) ); - XiaomiSmartHome_connect($hash) if( $init_done); + + InternalTimer(gettimeofday() + 5, "XiaomiSmartHome_connect", $hash, 0); return undef; } @@ -176,35 +265,57 @@ sub XiaomiSmartHome_Write($@) { my ($hash,$cmd,$val,$iohash) = @_; my $name = $hash->{NAME}; + if ( $hash->{helper}{ConnectionState} eq 'Disconnected') { + Log3 $name, 1, "$name: Write> Cannot write iam disconnected"; + return undef; + } + else{ + my $p = Net::Ping->new(); + if ( ! $p->ping($hash->{GATEWAY})){ + Log3 $name, 1, "$name: Write> Ping to $hash->{helper}{host} failed"; + $hash->{STATE} = "Disconnected"; + XiaomiSmartHome_disconnect($hash); + $p->close(); + return undef; + } + + } + my $GATEWAY = inet_ntoa(inet_aton($hash->{GATEWAY})); + $hash->{GATEWAY_IP} = $GATEWAY; my $msg; - if ($cmd eq 'read') + if ($cmd eq 'read') { $msg = '{"cmd":"' .$cmd . '","sid":"' . $val . '"}'; } - if ($cmd eq 'rgb') - { - # TODO SID des Gateway nicht im und aus dem Reading!! - $msg = '{"cmd":"write","model":"gateway","sid":"' . $hash->{READINGS}{HEARTBEAT}{VAL} . '","short_id":0,"key":"8","data":"{\"rgb\":' . $val . ',\"key\":\"'. XiaomiSmartHome_EncryptKey($hash) .'\"}" }'; - + else { + if ( $hash->{READINGS}{password}{VAL} !~ /^[a-zA-Z0-9]{16}$/ ) + { + Log3 $name, 1, "$name: Write> Password not SET!"; + readingsSingleUpdate($hash, "password", "giveaPassword!", 1); + return "for $cmd, wrong password, it must be hex and 16 characters"; + } + else { + if ($cmd eq 'rgb') + { + $msg = '{"cmd":"write","model":"gateway","sid":"' . $hash->{SID} . '","short_id":0,"key":"8","data":"{\"rgb\":' . $val . ',\"key\":\"'. XiaomiSmartHome_EncryptKey($hash) .'\"}" }'; + } + if ($cmd eq 'pct') + { + $msg = '{"cmd":"write","model":"gateway","sid":"' . $hash->{SID} . '","short_id":0,"key":"8","data":"{\"rgb\":' . $val . ',\"key\":\"'. XiaomiSmartHome_EncryptKey($hash) .'\"}" }'; + } + if ($cmd eq 'power') + { + $msg = '{"cmd":"write","model":"plug","sid":"' . $iohash->{SID} . '","data":"{\"status\":\"' . $val . '\",\"key\":\"'. XiaomiSmartHome_EncryptKey($hash) .'\"}" }'; + } } - if ($cmd eq 'illumination') - { - # TODO SID des Gateway nicht im und aus dem Reading!! - #$msg = '{"cmd":"write","model":"gateway","sid":"' . $hash->{READINGS}{HEARTBEAT}{VAL} . '","short_id":0,"key":"8","data":"{\"illumination\":\"' . $val . '\",\"key\":\"'. XiaomiSmartHome_EncryptKey($hash) .'\"}" }'; - $msg = '{"cmd":"write","model":"gateway","sid":"' . $hash->{READINGS}{HEARTBEAT}{VAL} . '","short_id":0,"key":"8","data":"{\"mid\":\"3\",\"key\":\"'. XiaomiSmartHome_EncryptKey($hash) .'\"}" }'; - #$msg = '{"cmd":"write","model":"gateway","sid":"' . $hash->{READINGS}{HEARTBEAT}{VAL} . '","short_id":0,"key":"8","data":"{\"hue\":\"170\",\"saturation\":\"254\", \"color_temperature\":\"65279\", \"x\":\"10\", \"y\":\"10\",\"key\":\"'. XiaomiSmartHome_EncryptKey($hash) .'\"}" }'; - } - if ($cmd eq 'power') - { - $msg = '{"cmd":"write","model":"plug","sid":"' . $iohash->{SID} . '","data":"{\"status\":\"' . $val . '\",\"key\":\"'. XiaomiSmartHome_EncryptKey($hash) .'\"}" }'; - } - return Log3 $name, 4, "Master ($name) - socket not connected" unless($hash->{CD}); + } + return Log3 $name, 4, "$name: Write> - socket not connected" unless($hash->{CD}); - Log3 $name, 4, "$name> $msg " . $hash->{GATEYWAY}; + Log3 $name, 3, "$name: Write> $msg " . $GATEWAY; my $sock = $hash->{CD}; my $MAXLEN = 1024; - $sock->mcast_send($msg,$hash->{GATEYWAY} .':9898') or die "send: $!"; - + $sock->mcast_send($msg,$GATEWAY .':9898') or die "send: $!"; + Log3 $name, 5, "$name: Write> End " . $GATEWAY; return undef; } ##################################### @@ -212,13 +323,18 @@ sub XiaomiSmartHome_Write($@) sub XiaomiSmartHome_EncryptKey($) { my ($hash) = @_; - if (defined $hash->{READINGS}{password}{VAL}){ + my $name = $hash->{NAME}; + if ( $hash->{READINGS}{password}{VAL} =~ /^[a-zA-Z0-9]{16}$/ ) { my $key = $hash->{READINGS}{password}{VAL}; my $cipher = Crypt::CBC->new(-key => $key, -cipher => 'Cipher::AES',-iv => $iv, -literal_key => 1, -header => "none", -keysize => 16 ); my $encryptkey = $cipher->encrypt_hex($hash->{READINGS}{token}{VAL}); $encryptkey = substr($encryptkey, 0, 32); return $encryptkey; } + else + { + Log3 $name, 1, "$name: EncryptKey> Password not SET!"; + } return undef; } ##################################### @@ -230,12 +346,12 @@ sub XiaomiSmartHome_Get($@) if ($opt eq "UpdateAll") { XiaomiSmartHome_updateAllReadings($hash); - Log3 $name, 5, "$name> UpdateALLReadings Started"; + Log3 $name, 5, "$name: Get> UpdateALLReadings Started"; } elsif($opt eq "UpdateSingle") { XiaomiSmartHome_updateSingleReading($hash,$args); - Log3 $name, 5, "$name> UpdateSingel Started"; + Log3 $name, 5, "$name: Get> UpdateSingel Started"; } else { @@ -248,15 +364,17 @@ sub XiaomiSmartHome_Notify($$) { my ($own_hash, $dev_hash) = @_; my $ownName = $own_hash->{NAME}; # own name / hash - Log3 $ownName, 3, "$ownName> NotifyStart"; + Log3 $ownName, 5, "$ownName: Notify> NotifyStart"; return "" if(IsDisabled($ownName)); # Return without any further action if the module is disabled - + $attr{$own_hash->{NAME}}{webCmd} = "pct:rgb:rgb ff0000:rgb 00ff00:rgb 0000ff:on:off" if ( ! $attr{$own_hash->{NAME}}{webCmd} || $attr{$own_hash->{NAME}}{webCmd} eq "rgb:rgb ff0000:rgb 00ff00:rgb 0000ff:on:off" ); + readingsSingleUpdate($own_hash, "pct", 100, 1) if ( ! $own_hash->{READINGS}{pct}{VAL}); my $devName = $dev_hash->{NAME}; # Device that created the events my $events = deviceEvents($dev_hash, 1); if($devName eq "global" && grep(m/^INITIALIZED|REREADCFG$/, @{$events})) { - Log3 $ownName, 3, "$ownName> Starting Connect"; - XiaomiSmartHome_connect($own_hash); + Log3 $ownName, 5, "$ownName: Notify> Starting Connect after global"; + XiaomiSmartHome_connect($own_hash); + CommandDeleteReading( undef, "$ownName HEARTBEAT" ) if(defined(ReadingsVal($ownName,"HEARTBEAT",undef))); } } ##################################### @@ -265,32 +383,46 @@ sub XiaomiSmartHome_Set($@) { my ( $hash, $name, $cmd, @args ) = @_; my $dec_num; + my $MIpct; my @match = grep( $_ =~ /^$cmd($|:)/, keys %sets ); #-- check argument return SetExtensions($hash, join(" ", keys %sets), $name, $cmd, @args) unless @match == 1; return "$cmd expects $sets{$match[0]} parameters" unless (@args eq $sets{$match[0]}); return "\"set $name\" needs at least one argument" unless(defined($cmd)); - - + if (!defined $hash->{READINGS}{pct}) { + $MIpct = $hash->{READINGS}{pct}{VAL}; + } + else { + $MIpct = 64; + } if($cmd eq "password") { - if($args[0] =~ /^[a-zA-Z0-9_.-]*$/) + #if($args[0] =~ /^[a-zA-Z0-9_.-]*$/) + if($args[0] =~ /^[a-zA-Z0-9]{16}$/) { readingsSingleUpdate( $hash, $cmd, $args[0], 0 ); return; } else { - return "Unknown value $args[0] for $cmd, wrong password, it must be hex"; + return "Unknown value $args[0] for $cmd, wrong password, it must be hex and 16 characters"; } } + if ( $hash->{READINGS}{password}{VAL} !~ /^[a-zA-Z0-9]{16}$/) + { + readingsSingleUpdate($hash, "password", "giveaPassword!", 1); + Log3 $name, 1, "$name: Set> Password not SET!"; + return "for $cmd, wrong password, it must be hex and 16 characters"; + } elsif($cmd eq "rgb") { my $ownName = $hash->{NAME}; - Log3 $ownName, 4, "$ownName> Set $cmd, $args[0]"; - $dec_num = sprintf("%d", hex('ff' . $args[0])); - Log3 $ownName, 4, "$ownName> Set $cmd, $dec_num"; + + Log3 $ownName, 4, "$ownName: Set> $cmd, $args[0]"; + $dec_num = sprintf("%d", hex($MIpct . $args[0])); + Log3 $ownName, 4, "$ownName: Set> $cmd, $dec_num"; + $hash->{helper}{prevrgbvalue} = $args[0]; readingsSingleUpdate( $hash, 'rgb', $args[0], 1 ); readingsSingleUpdate( $hash, 'state', 'on', 1 ); XiaomiSmartHome_Write($hash,$cmd,$dec_num); @@ -299,7 +431,7 @@ sub XiaomiSmartHome_Set($@) { $hash->{helper}{prevrgbvalue} = $hash->{READINGS}{rgb}{VAL}; readingsSingleUpdate( $hash, 'state', 'off', 1 ); - readingsSingleUpdate( $hash, 'rgb', 'off', 0 ); + readingsSingleUpdate( $hash, 'rgb', '000000', 0 ); XiaomiSmartHome_Write($hash,'rgb', 0); } elsif($cmd eq "on") @@ -308,7 +440,7 @@ sub XiaomiSmartHome_Set($@) readingsBulkUpdate( $hash, 'state', 'on', 1 ); if ($hash->{helper}{prevrgbvalue}) { - $dec_num = sprintf("%d", hex('ff' . $hash->{helper}{prevrgbvalue})); + $dec_num = sprintf("%d", hex($MIpct . $hash->{helper}{prevrgbvalue})); readingsBulkUpdate( $hash, 'rgb', $hash->{helper}{prevrgbvalue}, 1 ); XiaomiSmartHome_Write($hash,'rgb', $dec_num); } @@ -322,8 +454,14 @@ sub XiaomiSmartHome_Set($@) elsif($cmd eq "pct") { my $ownName = $hash->{NAME}; - Log3 $ownName, 4, "$ownName> Set $cmd, $args[0]"; - XiaomiSmartHome_Write($hash,'illumination',$args[0]); + Log3 $ownName, 4, "$ownName: Set> $cmd, $args[0], $hash->{helper}{prevrgbvalue}"; + readingsSingleUpdate( $hash, 'pct', $args[0], 1 ); + readingsSingleUpdate( $hash, 'rgb', $hash->{helper}{prevrgbvalue}, 1 ); + readingsSingleUpdate( $hash, 'state', 'on', 1 ); + my $MIpct = sprintf( "%.0f", (0.64 * $args[0])); + Log3 $ownName, 4, "$ownName: Set> $cmd, $MIpct"; + my $rgb = sprintf("%d", hex($MIpct . $hash->{helper}{prevrgbvalue})); + XiaomiSmartHome_Write($hash,'pct',$rgb); } else @@ -338,7 +476,17 @@ sub XiaomiSmartHome_Set($@) sub XiaomiSmartHome_Attr(@) { - + my ($cmd,$NAME,$name, $val) = @_; + if ($cmd eq "delete"){ + delete $attr{$NAME}{$name}; + CommandDeleteAttr(undef, "$NAME $name"); + Log3 $name, 1, "$name: Attr> delete $name"; + } + if ($cmd eq "create"){ + $attr{$NAME}{$name} = $val; + Log3 $name, 1, "$name: Attr> create $name $val"; + } + return undef; } ##################################### @@ -346,40 +494,58 @@ sub XiaomiSmartHome_connect($) { my $hash = shift; my $name = $hash->{NAME}; - Log3 $name, 3, "$name> ConnectStart"; + my $GATEWAY_IP; + Log3 $name, 5, "$name: connect> ConnectStart"; - XiaomiSmartHome_disconnect($hash); + Log3 $name, 4, "$name: connecting"; + my $p = Net::Ping->new(); + if ( ! $p->ping($hash->{GATEWAY})){ + Log3 $name, 1, "$name: connect> Ping to $hash->{helper}{host} failed"; + $hash->{STATE} = "Disconnected"; + XiaomiSmartHome_disconnect($hash); + $p->close(); + return undef; + } + if( $hash->{GATEWAY} !~ m/^\d+\.\d+\.\d+\.\d+$/ ){ + eval { + $GATEWAY_IP = inet_ntoa(inet_aton($hash->{GATEWAY})) ; + $hash->{GATEWAY_IP} = $GATEWAY_IP; + Log3 $name, 4, "$name: Connect> Set GATEWAYs IP: " . $GATEWAY_IP; + }; + if ($@) { + Log3 $name, 1, "$name: Connect> Error $@\n"; + $hash->{STATE} = "Disconnected"; + XiaomiSmartHome_disconnect($hash); + return undef; + } + } - Log3 $name, 4, "$name> connecting"; - - my $sock = IO::Socket::Multicast->new( Proto => 'udp', LocalPort =>'9898', ReuseAddr => 1) or die "Creating socket: $!\n"; - $sock->mcast_add('224.0.0.50', $hash->{fhemIP} ) || die "Couldn't set group: $!\n"; #$hash->{fhemIP} - $sock->mcast_ttl(32); - $sock->mcast_loopback(1); - - if ($sock) - { - Log3 $name, 3, "$name> connected"; - - $hash->{helper}{ConnectionState} = "Connected"; - - if ($hash->{helper}{ConnectionState} ne ReadingsVal($name, "state", "" )) - { - readingsSingleUpdate($hash, "state", $hash->{helper}{ConnectionState}, 1); - } - - $hash->{FD} = $sock->fileno(); + my $timeout = $hash->{TIMEOUT} ? $hash->{TIMEOUT} : 3; + my $sock = IO::Socket::Multicast->new( Proto => 'udp', LocalPort =>'9898', ReuseAddr => 1, Timeout => $timeout) or die "Creating socket: $!\n"; + $sock->setsockopt(SOL_SOCKET, SO_RCVTIMEO, pack('l!l!', 30, 0)) or die "setsockopt: $!"; + if ($sock) + { + Log3 $name, 3, "$name: connect> Connected"; + $sock->mcast_add('224.0.0.50', $hash->{fhemIP} ) || die "Couldn't set group: $!\n"; #$hash->{fhemIP} + $sock->mcast_ttl(32); + $sock->mcast_loopback(1); + $hash->{helper}{ConnectionState} = "Connected"; + $hash->{SID} = XiaomiSmartHome_getGatewaySID($hash); + if ($hash->{helper}{ConnectionState} ne ReadingsVal($name, "state", "" )) + { + readingsSingleUpdate($hash, "state", $hash->{helper}{ConnectionState}, 1); + } + $hash->{FD} = $sock->fileno(); $hash->{CD} = $sock; + $selectlist{$name} = $hash; + readingsSingleUpdate($hash, "password", 'giveaPassword!', 1) if(!defined $hash->{READINGS}{password}{VAL}); + InternalTimer(gettimeofday() + 7, "XiaomiSmartHome_updateAllReadings", $hash, 0); + } - $selectlist{$name} = $hash; - - XiaomiSmartHome_updateAllReadings($hash); - } - else - { - Log3 $name, 1, "$name> connect to $hash->{helper}{host} failed"; - } - + else + { + Log3 $name, 1, "$name: connect> connect to $hash->{helper}{host} failed"; + } return undef; } ##################################### @@ -397,11 +563,13 @@ sub XiaomiSmartHome_disconnect($) } return if (!$hash->{CD}); - Log3 $name, 3, "$name> disconnecting"; - - close($hash->{CD}); + Log3 $name, 3, "$name: disconnect> disconnecting"; + + close($hash->{CD}) if($hash->{CD}); + delete($hash->{FD}); delete($hash->{CD}); - + delete($selectlist{$name}); + return undef; } @@ -411,11 +579,11 @@ sub XiaomiSmartHome_updateSingleReading($$) { my ($hash, $sensor) = @_; my $name = $hash->{NAME}; - my $GATEYWAY = $hash->{GATEYWAY}; + my $GATEWAY = $hash->{GATEWAY}; my $sock = $hash->{CD}; my $MAXLEN = 1024; - Log3 $name, 4, "$name> PushSingelRead:" . $sensor; + Log3 $name, 4, "$name: updateSingleReading> PushSingelRead:" . $sensor; XiaomiSmartHome_Write($hash, 'read', $sensor); } @@ -425,30 +593,62 @@ sub XiaomiSmartHome_updateAllReadings($) { my $hash = shift; my $name = $hash->{NAME}; - my $GATEYWAY = $hash->{GATEYWAY}; + Log3 $name, 5, "name> updateAllReadings> Starting UpdateALLReadings"; + my $GATEWAY; + my $p = Net::Ping->new(); + if ( ! $p->ping($hash->{GATEWAY})){ + Log3 $name, 4, "$name: updateAllReadings> Ping to $hash->{helper}{host} failed"; + $hash->{STATE} = "Disconnected"; + XiaomiSmartHome_disconnect($hash); + $p->close(); + return undef; + } + if( $hash->{GATEWAY} !~ m/^\d+\.\d+\.\d+\.\d+$/ ){ + eval { + $GATEWAY = inet_ntoa(inet_aton($hash->{GATEWAY})) ; + Log3 $name, 4, "$name: updateAllReadings> Using DNS to IP: " . $GATEWAY; + }; + if ($@) { + Log3 $name, 1, "$name: updateAllReadings> Error $@\n"; + $hash->{STATE} = "Disconnected"; + XiaomiSmartHome_disconnect($hash); + return undef; + } + } + else + { + $GATEWAY = $hash->{GATEWAY}; + } + + if ( $hash->{helper}{ConnectionState} eq 'Disconnected') + { + Log3 $name, 1, "$name: updateAllReadings> Gateway is $hash->{STATE} trying to reconnect to $hash->{GATEWAY}"; + XiaomiSmartHome_connect($hash); + return undef; + } my $sock = $hash->{CD}; my $MAXLEN = 1024; my $msg = '{"cmd" : "get_id_list"}'; - $sock->mcast_send($msg, $GATEYWAY .':9898') or die "send: $!"; + $sock->mcast_send($msg, $GATEWAY .':9898') or die "send: $!"; eval { $sock->recv($msg, $MAXLEN) or die "recv: $!"; - Log3 $name, 5, "$name> " . $msg; + Log3 $name, 5, "$name: updateAllReadings>" . $msg; my $json = $hash->{helper}{JSON}->incr_parse($msg); my $decoded = decode_json($msg); if ($json){ - Log3 $name, 5, "$name> Read:" . $msg; + Log3 $name, 5, "$name: updateAllReadings> Read:" . $msg; if ($decoded->{'cmd'} eq 'get_id_list_ack'){ my @sensors = @{decode_json($decoded->{data})}; foreach my $sensor (@sensors) { $msg = '{"cmd":"read","sid":"' . $sensor . '" }'; - Log3 $name, 4, "$name> PushRead:" . $sensor; + Log3 $name, 4, "$name: updateAllReadings> PushRead:" . $sensor; my $msg = '{"cmd":"read","sid":"' . $sensor . '" }'; - $sock->mcast_send($msg, $GATEYWAY .':9898') or die "send: $!"; + $sock->mcast_send($msg, $GATEWAY .':9898') or die "send: $!"; eval { $sock->recv($msg, $MAXLEN) or die "recv: $!"; - Log3 $name, 5, "$name> " . $msg; + Log3 $name, 5, "$name: updateAllReadings> " . $msg; Dispatch($hash, $msg, undef); } @@ -456,6 +656,7 @@ sub XiaomiSmartHome_updateAllReadings($) } } } + #return undef; } ##################################### @@ -463,8 +664,8 @@ sub XiaomiSmartHome_updateAllReadings($) =pod =item device -=item summary Module fpr XiaomiSmartHome Gateway to use with FHEM -=item summary_DE Modul um ein XiaomiSmartHome Gateyway in FHEM zu nutzen +=item summary Module for XiaomiSmartHome Gateway to use with FHEM +=item summary_DE Modul um ein XiaomiSmartHome Gateway in FHEM zu nutzen =begin html diff --git a/FHEM/71_XiaomiSmartHome_Device.pm b/FHEM/71_XiaomiSmartHome_Device.pm index 3af88da..b484c71 100644 --- a/FHEM/71_XiaomiSmartHome_Device.pm +++ b/FHEM/71_XiaomiSmartHome_Device.pm @@ -25,23 +25,24 @@ package main; use strict; use warnings; -my $version = "0.11"; +my $version = "0.18"; sub XiaomiSmartHome_Device_updateSReading($); -##################################### + +##################################### sub XiaomiSmartHome_Device_Initialize($) { my ($hash) = @_; - $hash->{Match} = "^.+magnet|motion|sensor_ht|switch|plug|cube"; + $hash->{Match} = "^.+magnet|motion|sensor_ht|switch|plug|cube|86sw2"; $hash->{DefFn} = "XiaomiSmartHome_Device_Define"; $hash->{SetFn} = "XiaomiSmartHome_Device_Set"; $hash->{UndefFn} = "XiaomiSmartHome_Device_Undef"; $hash->{ParseFn} = "XiaomiSmartHome_Device_Parse"; $hash->{AttrList} = "IODev follow-on-for-timer:1,0 follow-on-timer ". - "do_not_notify:1,0 ignore:1,0 dummy:1,0 showtime:1,0 valueFn:textField-long motionOffTimer:1,5,10 ". + "do_not_notify:1,0 ignore:1,0 dummy:1,0 showtime:1,0 valueFn:textField-long". $readingFnAttributes ; } ##################################### @@ -64,6 +65,8 @@ sub XiaomiSmartHome_Device_Set($@) return "\"set $name\" needs at least one argument" unless(defined($cmd)); my $setlist = ""; + $setlist .= "motionOffTimer:1,5,10 " if ($hash->{MODEL} eq 'motion'); + #$setlist = "open:noArg close:noArg " if ($hash->{MODEL} eq 'magnet'); $setlist .= "power:on,off " if ($hash->{MODEL} eq 'plug'); if($cmd eq "power") @@ -77,6 +80,16 @@ sub XiaomiSmartHome_Device_Set($@) IOWrite($hash,"power","off",$hash); } } + if($cmd eq "open") + { + readingsSingleUpdate($hash, "state", "open", 1 ); + return; + } + if($cmd eq "motionOffTimer") + { + readingsSingleUpdate($hash, "motionOffTimer", "$args[0]", 1 );; + return; + } else { return "Unknown argument $cmd, choose one of $setlist"; @@ -91,7 +104,7 @@ sub XiaomiSmartHome_Device_on_timeout($){ my $name = $hash->{LASTInputDev}; if ($hash->{STATE} eq 'motion') { readingsSingleUpdate($hash, "state", "off", 1 ); - Log3 $name, 3, "$name>" . " SID: " . $hash->{SID} . " Type: " . $hash->{MODEL} . " Status: off"; + Log3 $name, 3, "$name: DEV_Timeout>" . " SID: " . $hash->{SID} . " Type: " . $hash->{MODEL} . " Status: off"; } } ##################################### @@ -100,11 +113,11 @@ sub XiaomiSmartHome_Device_Read($$$){ my $decoded = decode_json($msg); my $sid = $decoded->{'sid'}; my $model = $decoded->{'model'}; - Log3 $name, 5, "$name: SID: " . $hash->{SID} . " " . $hash->{TYPE}; + Log3 $name, 5, "$name: DEV_Read> SID: " . $hash->{SID} . " " . $hash->{TYPE}; my $data = decode_json($decoded->{data}); readingsBeginUpdate( $hash ); if (defined $data->{status}){ - Log3 $name, 3, "$name>" . " SID: " . $sid . " Type: " . $hash->{MODEL} . " Status: " . $data->{status}; + Log3 $name, 3, "$name: DEV_Read>" . " SID: " . $sid . " Type: " . $hash->{MODEL} . " Status: " . $data->{status}; readingsBulkUpdate($hash, "state", "$data->{status}", 1 ); if ($data->{status} eq 'motion' && $hash->{MODEL} eq 'motion'){ readingsBulkUpdate($hash, "no_motion", "0", 1 ); @@ -114,51 +127,63 @@ sub XiaomiSmartHome_Device_Read($$$){ } } if (defined $data->{no_motion}){ - Log3 $name, 3, "$name>" . " SID: " . $sid . " Type: " . $hash->{MODEL} . " NO_motion: " . $data->{no_motion}; + Log3 $name, 3, "$name: DEV_Read>" . " SID: " . $sid . " Type: " . $hash->{MODEL} . " NO_motion: " . $data->{no_motion}; readingsBulkUpdate($hash, "no_motion", "$data->{no_motion}", 1 ); } if (defined $data->{no_close}){ - Log3 $name, 3, "$name>" . " SID: " . $sid . " Type: " . $hash->{MODEL} . " NO_close: " . $data->{no_close}; + Log3 $name, 3, "$name: DEV_Read>" . " SID: " . $sid . " Type: " . $hash->{MODEL} . " NO_close: " . $data->{no_close}; readingsBulkUpdate($hash, "no_close", "$data->{no_close}", 1 ); } if (defined $data->{voltage}){ - Log3 $name, 3, "$name>" . " SID: " . $sid . " Type: " . $hash->{MODEL} . " Voltage: " . $data->{voltage}; - readingsBulkUpdate($hash, "voltage", "$data->{voltage}", 1 ); + my $bat = ($data->{voltage}/1000); + Log3 $name, 3, "$name: DEV_Read>" . " SID: " . $sid . " Type: " . $hash->{MODEL} . " Voltage: " . $data->{voltage}; + readingsBulkUpdate($hash, "battery", $bat, 1 ); } if (defined $data->{temperature}){ my $temp = $data->{temperature}; $temp =~ s/(^[-+]?\d+?(?=(?>(?:\d{2})+)(?!\d))|\G\d{2}(?=\d))/$1./g; - Log3 $name, 3, "$name>" . " SID: " . $sid . " Type: " . $hash->{MODEL} . " Temperature: " . $temp; + Log3 $name, 3, "$name: DEV_Read>" . " SID: " . $sid . " Type: " . $hash->{MODEL} . " Temperature: " . $temp; readingsBulkUpdate($hash, "temperature", "$temp", 1 ); } if (defined $data->{humidity}){ my $hum = $data->{humidity}; $hum =~ s/(^[-+]?\d+?(?=(?>(?:\d{2})+)(?!\d))|\G\d{2}(?=\d))/$1./g; - Log3 $name, 3, "$name>" . " SID: " . $sid . " Type: " . $hash->{MODEL} . " Humidity: " . $hum; + Log3 $name, 3, "$name: DEV_Read>" . " SID: " . $sid . " Type: " . $hash->{MODEL} . " Humidity: " . $hum; readingsBulkUpdate($hash, "humidity", "$hum", 1 ); } + #86sw2 start + if (defined $data->{channel_0}){ + Log3 $name, 3, "$name: DEV_Read>" . " SID: " . $sid . " Type: " . $hash->{MODEL} . " Channel_0: " . $data->{channel_0}; + readingsBulkUpdate($hash, "channel_0", "$data->{channel_0}", 1 ); + } + if (defined $data->{channel_1}){ + Log3 $name, 3, "$name: DEV_Read>" . " SID: " . $sid . " Type: " . $hash->{MODEL} . " Channel_1: " . $data->{channel_1}; + readingsBulkUpdate($hash, "channel_1", "$data->{channel_1}", 1 ); + } + #86sw2 end #plug start if (defined $data->{load_voltage}){ - Log3 $name, 3, "$name>" . " SID: " . $sid . " Type: " . $hash->{MODEL} . " LOAD_Voltage: " . $data->{load_voltage}; + Log3 $name, 3, "$name: DEV_Read>" . " SID: " . $sid . " Type: " . $hash->{MODEL} . " LOAD_Voltage: " . $data->{load_voltage}; readingsBulkUpdate($hash, "LOAD_Voltage", "$data->{load_voltage}", 1 ); } if (defined $data->{load_power}){ - Log3 $name, 3, "$name>" . " SID: " . $sid . " Type: " . $hash->{MODEL} . " LOAD_Power: " . $data->{load_power}; + Log3 $name, 3, "$name: DEV_Read>" . " SID: " . $sid . " Type: " . $hash->{MODEL} . " LOAD_Power: " . $data->{load_power}; readingsBulkUpdate($hash, "LOAD_Power", "$data->{load_power}", 1 ); } if (defined $data->{power_consumed}){ - Log3 $name, 3, "$name>" . " SID: " . $sid . " Type: " . $hash->{MODEL} . " POWER_Consumed: " . $data->{power_consumed}; + Log3 $name, 3, "$name:" . " SID: " . $sid . " Type: " . $hash->{MODEL} . " POWER_Consumed: " . $data->{power_consumed}; readingsBulkUpdate($hash, "POWER_Consumed", "$data->{power_consumed}", 1 ); } if (defined $data->{inuse}){ - Log3 $name, 3, "$name>" . " SID: " . $sid . " Type: " . $hash->{MODEL} . " InUse: " . $data->{inuse}; + Log3 $name, 3, "$name: DEV_Read>" . " SID: " . $sid . " Type: " . $hash->{MODEL} . " InUse: " . $data->{inuse}; readingsBulkUpdate($hash, "inuse", "$data->{inuse}", 1 ); } #plug end #cube start if (defined $data->{rotate}){ - Log3 $name, 3, "$name>" . " SID: " . $sid . " Type: " . $hash->{MODEL} . " Rotate: " . $data->{rotate}; + Log3 $name, 3, "$name: DEV_Read>" . " SID: " . $sid . " Type: " . $hash->{MODEL} . " Rotate: " . $data->{rotate}; readingsBulkUpdate($hash, "rotate", "$data->{rotate}", 1 ); + readingsBulkUpdate($hash, "state", "rotate", 1 ); } #cube end if ($decoded->{'cmd'} eq 'heartbeat'){ @@ -180,13 +205,13 @@ sub XiaomiSmartHome_Device_Parse($$) { if (my $io_hash = $modules{XiaomiSmartHome_Device}{defptr}{$sid}) { - Log3 $name, 4, "$name> IS DEFINED " . $model . " : " .$sid; + Log3 $name, 4, "$name: DEV_Parse> IS DEFINED " . $model . " : " .$sid; XiaomiSmartHome_Device_Read($io_hash, $msg, $name); } else { - Log3 $name, 4, "$name> UNDEFINED " . $model . " : " .$sid; + Log3 $name, 4, "$name: DEV_Parse> UNDEFINED " . $model . " : " .$sid; return "UNDEFINED XMI_$sid XiaomiSmartHome_Device $sid $model $name"; } } @@ -197,18 +222,21 @@ sub XiaomiSmartHome_Device_update($){ my $model = $hash->{MODEL}; my $name = $hash->{NAME}; my $value_fn = AttrVal( $name, "valueFn", "" ); - my $mot = AttrVal( $name, "motionOffTimer", "" ); + #my $mot = AttrVal( $name, "motionOffTimer", "" ); + my $mot = $hash->{READINGS}{motionOffTimer}{VAL} if ($hash->{READINGS}{motionOffTimer}); if( $value_fn =~ m/^{.*}$/s ) { my $LASTCMD = ReadingsVal($name,"lastCmd",undef); my $value_fn = eval $value_fn; - Log3 $name, 3, $name .": valueFn: ". $@ if($@); + Log3 $name, 3, $name .": DEV_Update valueFn: ". $@ if($@); return undef if( !defined($value_fn) ); } if( $model eq 'motion') { - XiaomiSmartHome_Device_mot($hash, $mot) if( $mot); + XiaomiSmartHome_Device_mot($hash, $hash->{READINGS}{motionOffTimer}{VAL}) if( $hash->{READINGS}{motionOffTimer}); } + # Update delete old reading Voltage + CommandDeleteReading( undef, "$name voltage" ) if(defined(ReadingsVal($name,"voltage",undef))); } ##################################### @@ -228,9 +256,9 @@ sub XiaomiSmartHome_Device_Define($$) { if(defined($hash->{IODev}->{NAME})) { my $IOname = $hash->{IODev}->{NAME}; - Log3 $name, 3, $IOname . "> " .$name. ": " . $type . " I/O device is " . $hash->{IODev}->{NAME}; + Log3 $name, 3, $IOname . ": DEV_Define> " .$name. ": " . $type . " I/O device is " . $hash->{IODev}->{NAME}; } else { - Log3 $name, 1, "$name $type - no I/O device"; + Log3 $name, 1, "$name DEV_Define> $type - no I/O device"; } $iodev = $hash->{IODev}->{NAME}; @@ -238,7 +266,7 @@ sub XiaomiSmartHome_Device_Define($$) { return "XiaomiSmartHome device $hash->{SID} on XiaomiSmartHome $iodev already defined as $d->{NAME}." if( defined($d) && $d->{IODev} == $hash->{IODev} && $d->{NAME} ne $name ); - Log3 $name, 3, $iodev . "> " . $name . ": defined as ". $hash->{MODEL}; + Log3 $name, 3, $iodev . ": DEV_Define> " . $name . ": defined as ". $hash->{MODEL}; $attr{$name}{room} = "MiSmartHome" if( !defined( $attr{$name}{room} ) ); if( $type eq 'motion') { $attr{$name}{devStateIcon} = 'motion:motion_detector@red off:motion_detector@green no_motion:motion_detector@green' if( !defined( $attr{$name}{devStateIcon} ) ); @@ -251,8 +279,8 @@ sub XiaomiSmartHome_Device_Define($$) { } if( $init_done ) { - InternalTimer( gettimeofday()+int(rand(2)), "XiaomiSmartHome_Device_updateSReading", $hash, 0 ); - Log3 $name, 4, $iodev . "> " . $name . " Init Done set InternalTimer for Update"; + InternalTimer(gettimeofday() + 2, "XiaomiSmartHome_Device_updateSReading", $hash, 0 ); + Log3 $name, 4, $iodev . ": DEV_Define> " . $name . " Init Done set InternalTimer for Update"; } return undef; } @@ -260,8 +288,8 @@ sub XiaomiSmartHome_Device_Define($$) { sub XiaomiSmartHome_Device_updateSReading($) { my $hash = shift; - #my $name = $hash->{NAME}; - #Log3 $name, 3, $name . " Updae SR"; + my $name = $hash->{NAME}; + Log3 $name, 3, "$name: DEV_updateSReading> for $hash->{SID}"; RemoveInternalTimer($hash,'XiaomiSmartHome_Device_updateSReading'); IOWrite($hash,'read',"$hash->{SID}"); } @@ -275,7 +303,7 @@ sub XiaomiSmartHome_Device_Undef($) my $iodev = $hash->{IODev}->{NAME}; RemoveInternalTimer($hash); delete($modules{XiaomiSmartHome_Device}{defptr}{$hash->{SID}}); - Log3 $name, 3, "$iodev> $name - device deleted"; + Log3 $name, 3, "$iodev: DEV_Undef> $name - device deleted"; return undef; } diff --git a/controls_mish.txt b/controls_mish.txt index 65ccbf5..1de5475 100644 --- a/controls_mish.txt +++ b/controls_mish.txt @@ -1,2 +1,2 @@ -UPD 2017-04-03_14:31:37 15333 FHEM/71_XiaomiSmartHome.pm -UPD 2017-04-03_22:28:16 10007 FHEM/71_XiaomiSmartHome_Device.pm \ No newline at end of file +UPD 2017-04-28_20:38:35 22335 FHEM/71_XiaomiSmartHome.pm +UPD 2017-04-28_20:38:37 11413 FHEM/71_XiaomiSmartHome_Device.pm \ No newline at end of file