# $Id$ ############################################################################## # # 72_FB_CALLMONITOR.pm # Connects to a FritzBox Fon via network. # When a call is received or takes place it creates an event with further call informations. # This module has no sets or gets as it is only used for event triggering. # # Copyright by Markus Bloch # e-mail: Notausstieg0309@googlemail.com # # This file is part of fhem. # # Fhem is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 2 of the License, or # (at your option) any later version. # # Fhem is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with fhem. If not, see . # ############################################################################## package main; use strict; use warnings; use Time::HiRes qw(gettimeofday); use DevIo; use Digest::MD5; use HttpUtils; my %connection_type = ( 0 => "0", 1 => "FON1", 2 => "FON2", 3 => "FON3", 4 => "ISDN", 5 => "FAX", 6 => "not_defined", 7 => "not_defined", 8 => "not_defined", 9 => "not_defined", 10 => "DECT_1", 11 => "DECT_2", 12 => "DECT_3", 13 => "DECT_4", 14 => "DECT_5", 15 => "DECT_6", 16 => "FRITZMini_1", 17 => "FRITZMini_2", 18 => "FRITZMini_3", 19 => "FRITZMini_4", 20 => "VoIP_1", 21 => "VoIP_2", 22 => "VoIP_3", 23 => "VoIP_4", 24 => "VoIP_5", 25 => "VoIP_6", 26 => "VoIP_7", 27 => "VoIP_8", 28 => "VoIP_9", 29 => "VoIP_10", 40 => "Answering_Machine_1", 41 => "Answering_Machine_2", 42 => "Answering_Machine_3", 43 => "Answering_Machine_4", 44 => "Answering_Machine_5" ); sub FB_CALLMONITOR_Initialize($) { my ($hash) = @_; require "$attr{global}{modpath}/FHEM/DevIo.pm"; # Provider $hash->{ReadFn} = "FB_CALLMONITOR_Read"; $hash->{ReadyFn} = "FB_CALLMONITOR_Ready"; $hash->{GetFn} = "FB_CALLMONITOR_Get"; $hash->{SetFn} = "FB_CALLMONITOR_Set"; $hash->{DefFn} = "FB_CALLMONITOR_Define"; $hash->{UndefFn} = "FB_CALLMONITOR_Undef"; $hash->{AttrFn} = "FB_CALLMONITOR_Attr"; $hash->{AttrList}= "do_not_notify:0,1 unique-call-ids:0,1 local-area-code remove-leading-zero:0,1 reverse-search-cache-file reverse-search:all,internal,klicktel.de,dasoertliche.de,search.ch,dasschnelle.at,none reverse-search-cache:0,1 reverse-search-phonebook-file ". $readingFnAttributes; } ##################################### sub FB_CALLMONITOR_Define($$) { my ($hash, $def) = @_; my @a = split("[ \t][ \t]*", $def); if(@a != 3) { my $msg = "wrong syntax: define FB_CALLMONITOR ip[:port]"; Log 2, $msg; return $msg; } DevIo_CloseDev($hash); my $name = $a[0]; my $dev = $a[2]; $dev .= ":1012" if($dev !~ m/:/ && $dev ne "none" && $dev !~ m/\@/); $hash->{DeviceName} = $dev; my $ret = DevIo_OpenDev($hash, 0, "FB_CALLMONITOR_DoInit"); return $ret; } ##################################### sub FB_CALLMONITOR_Undef($$) { my ($hash, $arg) = @_; my $name = $hash->{NAME}; DevIo_CloseDev($hash); return undef; } ##################################### # No get commands possible, as we just receive the events from the FritzBox. sub FB_CALLMONITOR_Get($@) { my ($hash, @arguments) = @_; return "argument missing" if(int(@arguments) < 2); if($arguments[1] eq "search") { if($arguments[2] =~ /^\d+$/) { return FB_CALLMONITOR_reverseSearch($hash, $arguments[2]); } else { return "given argument is not a telephone number"; } } else { return "unknown argument, choose on of search"; } } sub FB_CALLMONITOR_Set($@) { my ($hash, @a) = @_; my $usage = (defined($hash->{helper}{PHONEBOOK}) ? "Unknown argument ".$a[1].", choose one of rereadPhonebook" : ""); if($a[1] eq "rereadPhonebook") { FB_CALLMONITOR_loadInternalPhonebookFile($hash); return undef; } else { return $usage; } } ##################################### # Receives an event and creates several readings for event triggering sub FB_CALLMONITOR_Read($) { my ($hash) = @_; my $buf = DevIo_SimpleRead($hash); return "" if(!defined($buf)); my $name = $hash->{NAME}; my @array; my $reverse_search = undef; my $data = $buf; my $area_code = AttrVal($name, "local-area-code", ""); my $external_number = undef; @array = split(";", $data); $external_number = $array[3] if(not $array[3] eq "0" and $array[1] eq "RING" and $array[3] ne ""); $external_number = $array[5] if($array[1] eq "CALL" and $array[3] ne ""); $external_number =~ s/^0// if(AttrVal($name, "remove-leading-zero", "0") eq "1" and defined($external_number)); if(defined($external_number) and not $external_number =~ /^0/ and $area_code ne "") { if($area_code =~ /^0[1-9]\d+$/) { $external_number = $area_code.$external_number; } else { Log3 $name, 2, "$name: given local area code '$area_code' is not an area code. therefore will be ignored"; } } # Remove trailing hash sign and everything afterwards $external_number =~ s/#.*$// if(defined($external_number)); $reverse_search = FB_CALLMONITOR_reverseSearch($hash, $external_number) if(defined($external_number) and AttrVal($name, "reverse-search", "none") ne "none"); readingsBeginUpdate($hash); readingsBulkUpdate($hash, "event", lc($array[1])); readingsBulkUpdate($hash, "external_number", (defined($external_number) ? $external_number : "unknown")) if($array[1] eq "RING" or $array[1] eq "CALL"); readingsBulkUpdate($hash, "external_name",(defined($reverse_search) ? $reverse_search : "unknown")) if($array[1] eq "RING" or $array[1] eq "CALL"); readingsBulkUpdate($hash, "internal_number", $array[4]) if($array[1] eq "RING" or $array[1] eq "CALL"); readingsBulkUpdate($hash, "external_connection", $array[5]) if($array[1] eq "RING"); readingsBulkUpdate($hash, "external_connection", $array[6]) if($array[1] eq "CALL"); readingsBulkUpdate($hash, "internal_connection", $connection_type{$array[3]}) if($array[1] eq "CALL" or $array[1] eq "CONNECT" and defined($connection_type{$array[3]})); readingsBulkUpdate($hash, "call_duration", $array[3]) if($array[1] eq "DISCONNECT"); if ($array[1] eq "RING") { $hash->{helper}{MISSED_CALL_DETECTION}{$array[2]}{EVENT} = $array[1]; my $no = "unknown"; if (defined($external_number)) { $no = $external_number; if (defined($reverse_search)) { $no .= " (".$reverse_search.")"; } } $hash->{helper}{MISSED_CALL_DETECTION}{$array[2]}{NUMBER} = $no; } elsif ($array[1] eq "DISCONNECT") { if (($array[3] eq "0") and (exists($hash->{helper}{MISSED_CALL_DETECTION}{$array[2]}) and $hash->{helper}{MISSED_CALL_DETECTION}{$array[2]}{EVENT} eq "RING")) { readingsBulkUpdate($hash, "missed_call", $hash->{helper}{MISSED_CALL_DETECTION}{$array[2]}{NUMBER}) } delete($hash->{helper}{MISSED_CALL_DETECTION}{$array[2]}) if(exists($hash->{helper}{MISSED_CALL_DETECTION}{$array[2]})); } if(AttrVal($name, "unique-call-ids", "0") eq "1") { if($array[1] eq "RING" or $array[1] eq "CALL") { $hash->{helper}{CALLID}{$array[2]} = Digest::MD5::md5_hex($data); } readingsBulkUpdate($hash, "call_id", $hash->{helper}{CALLID}{$array[2]}); if($array[1] eq "DISCONNECT") { delete($hash->{helper}{CALLID}{$array[2]}); } } else { readingsBulkUpdate($hash, "call_id", $array[2]); } readingsEndUpdate($hash, 1); } sub FB_CALLMONITOR_DoInit($) { # No Initialization needed return undef; } sub FB_CALLMONITOR_Ready($) { my ($hash) = @_; return DevIo_OpenDev($hash, 1, "FB_CALLMONITOR_DoInit"); } sub FB_CALLMONITOR_Attr($@) { my (@a) = @_; my $hash = $defs{$a[1]}; my $name = $hash->{NAME}; if($a[0] eq "set") { if($a[2] eq "reverse-search" or $a[2] eq "reverse-search-phonebook-file") { $attr{$name}{"reverse-search-phonebook-file"} = $a[3] if($a[2] eq "reverse-search-phonebook-file"); FB_CALLMONITOR_loadInternalPhonebookFile($hash); } if($a[2] eq "reverse-search-cache-file") { $attr{$name}{"reverse-search-cache-file"} = $a[3]; FB_CALLMONITOR_loadCacheFile($hash); } } elsif($a[0] eq "del") { if($a[2] eq "reverse-search" or $a[2] eq "reverse-search-phonebook-file") { delete($hash->{helper}{PHONEBOOK}) if(defined($hash->{helper}{PHONEBOOK})); } } return undef; } sub FB_CALLMONITOR_reverseSearch($$) { my ($hash, $number) = @_; my $name = $hash->{NAME}; my $result; my $invert_match = undef; chomp $number; # Using internal phonebook if available and enabled if(AttrVal($name, "reverse-search", "none") eq "all" or AttrVal($name, "reverse-search", "none") eq "internal" and defined($hash->{helper}{PHONEBOOK})) { if(defined($hash->{helper}{PHONEBOOK}{$number})) { Log3 $name, 4, "FB_CALLMONITOR $name using internal phonebook for reverse search of $number"; return $hash->{helper}{PHONEBOOK}{$number}; } } # Using Cache if enabled if(AttrVal($name, "reverse-search-cache", "0") eq "1") { if(defined($hash->{helper}{CACHE}{$number})) { Log3 $name, 4, "FB_CALLMONITOR $name using cache for reverse search of $number"; if($hash->{helper}{CACHE}{$number} ne "timeout") { return $hash->{helper}{CACHE}{$number}; } } } # Ask klicktel.de if(AttrVal($name, "reverse-search", "none") eq "all" or AttrVal($name, "reverse-search", "none") eq "klicktel.de") { Log3 $name, 4, "FB_CALLMONITOR: $name using klicktel.de for reverse search of $number"; $result = GetFileFromURL("http://www.klicktel.de/inverssuche/index/search?_dvform_posted=1&phoneNumber=".$number, 5, undef, 1); if(not defined($result)) { if(AttrVal($name, "reverse-search-cache", "0") eq "1") { $hash->{helper}{CACHE}{$number} = "timeout"; undef($result); return "timeout"; } } else { if($result =~ /(.+?)<\/a>/) { $invert_match = $1; $invert_match = FB_CALLMONITOR_html2txt($invert_match); FB_CALLMONITOR_writeToCache($hash, $number, $invert_match) if(AttrVal($name, "reverse-search-cache", "0") eq "1"); undef($result); return $invert_match; } } } # Ask dasoertliche.de if(AttrVal($name, "reverse-search", "none") eq "all" or AttrVal($name, "reverse-search", "none") eq "dasoertliche.de") { Log3 $name, 4, "FB_CALLMONITOR: $name using dasoertliche.de for reverse search of $number"; $result = GetFileFromURL("http://www1.dasoertliche.de/?form_name=search_inv&ph=".$number, 5, undef, 1); if(not defined($result)) { if(AttrVal($name, "reverse-search-cache", "0") eq "1") { $hash->{helper}{CACHE}{$number} = "timeout"; undef($result); return "timeout"; } } else { #Log 2, $result; if($result =~ /getItemData\('.*?', '.*?', '.*?', '.*?', '.*?', '(.*?)', '.*?', '.*?', '.*?'\);/) { $invert_match = $1; $invert_match = FB_CALLMONITOR_html2txt($invert_match); FB_CALLMONITOR_writeToCache($hash, $number, $invert_match) if(AttrVal($name, "reverse-search-cache", "0") eq "1"); undef($result); return $invert_match; } } } # SWITZERLAND ONLY!!! Ask search.ch if(AttrVal($name, "reverse-search", "none") eq "search.ch") { Log3 $name, 4, "FB_CALLMONITOR: $name using search.ch for reverse search of $number"; $result = GetFileFromURL("http://tel.search.ch/?tel=".$number, 5, undef, 1); if(not defined($result)) { if(AttrVal($name, "reverse-search-cache", "0") eq "1") { $hash->{helper}{CACHE}{$number} = "timeout"; undef($result); return "timeout"; } } else { #Log 2, $result; if($result =~ /
(.+?)<\/a><\/h5>/) { $invert_match = $1; $invert_match = FB_CALLMONITOR_html2txt($invert_match); FB_CALLMONITOR_writeToCache($hash, $number, $invert_match) if(AttrVal($name, "reverse-search-cache", "0") eq "1"); undef($result); return $invert_match; } } } # Austria ONLY!!! Ask dasschnelle.at if(AttrVal($name, "reverse-search", "none") eq "dasschnelle.at") { Log3 $name, 4, "FB_CALLMONITOR: $name using dasschnelle.at for reverse search of $number"; $result = GetFileFromURL("http://www.dasschnelle.at/result/index/results?PerPage=5&pageNum=1&what=".$number."&where=&rubrik=0&bezirk=0&orderBy=Standard&mapsearch=false", 5, undef, 1); if(not defined($result)) { if(AttrVal($name, "reverse-search-cache", "0") eq "1") { $hash->{helper}{CACHE}{$number} = "timeout"; undef($result); return "timeout"; } } else { #Log 2, $result; if($result =~ /name\s+:\s+"(.+?)",/) { $invert_match = ""; while($result =~ /name\s+:\s+"(.+?)",/g) { $invert_match = $1 if(length($1) > length($invert_match)); } $invert_match = FB_CALLMONITOR_html2txt($invert_match); FB_CALLMONITOR_writeToCache($hash, $number, $invert_match) if(AttrVal($name, "reverse-search-cache", "0") eq "1"); undef($result); return $invert_match; } } } if(AttrVal($name, "reverse-search-cache", "0") eq "1") { # If no result is available set cache result and return undefined $hash->{helper}{CACHE}{$number} = "unknown"; } return undef; } sub FB_CALLMONITOR_html2txt($) { my ($string) = @_; $string =~ s/ / /g; $string =~ s/&/&/g; $string =~ s/(\xe4|ä)/ä/g; $string =~ s/(\xc4|Ä)/Ä/g; $string =~ s/(\xf6|ö)/ö/g; $string =~ s/(\xd6|Ö)/Ö/g; $string =~ s/(\xfc|ü)/ü/g; $string =~ s/(\xdc|Ü)/Ü/g; $string =~ s/(\xdf|ß)/ß/g; $string =~ s/<.+?>//g; $string =~ s/(^\s+|\s+$)//g; return $string; } sub FB_CALLMONITOR_writeToCache($$$) { my ($hash, $number, $txt) = @_; my $name = $hash->{NAME}; my $file = AttrVal($name, "reverse-search-cache-file", ""); $file =~ s/(^\s+|\s+$)//g; $hash->{helper}{CACHE}{$number} = $txt; if($file ne "") { Log3 $name, 4, "FB_CALLMONITOR: $name opening cache file $file"; if(open(CACHEFILE, ">>$file")) { print CACHEFILE "$number|$txt\n"; close(CACHEFILE); } else { Log3 $name, 2, "FB_CALLMONITOR: $name could not open cache file"; } } } sub FB_CALLMONITOR_loadInternalPhonebookFile($@) { my ($hash, $overwrite) = @_; my $name = $hash->{NAME}; my $phonebook = undef; my $contact; my $contact_name; my $number; my $count_contacts; my $area_code = AttrVal($name, "local-area-code", ""); my $phonebook_file = AttrVal($name, "reverse-search-phonebook-file", "/var/flash/phonebook"); $overwrite = 1 unless(defined($overwrite)); return if($overwrite == 0 and defined($hash->{helper}{PHONEBOOK})); if(-r $phonebook_file) { delete $hash->{helper}{PHONEBOOK} if(defined($hash->{helper}{PHONEBOOK})); if(open(PHONEBOOK, "<$phonebook_file")) { $phonebook = join('', ); if($phonebook =~ // and $phonebook =~ //) { Log3 $name, 2, "FB_CALLMONITOR: $name found FritzBox phonebook $phonebook_file"; while($phonebook =~ m/]*>(.+?)<\/contact>/gs) { $contact = $1; if($contact =~ m/(.+?)<\/realName>/) { $contact_name = $1; Log3 $name, 4, "FB_CALLMONITOR: $name found $contact_name"; while($contact =~ m/]*?type="([^<>"]+?)"[^<>]*?>([^<>"]+?)<\/number>/gs) { if($1 ne "intern" and $1 ne "memo") { $number = $2; $number =~ s/^\+\d\d/0/g; # quick'n'dirty fix in case of international number format. $number =~ s/\D//g unless($number =~ /@/); $number =~ s/\s//g if($number =~ /@/); if(not $number =~ /^0/ and not $number =~ /@/ and $area_code ne "") { if($area_code =~ /^0[1-9]\d+$/) { $number = $area_code.$number; } } $hash->{helper}{PHONEBOOK}{$number} = FB_CALLMONITOR_html2txt($contact_name) if(not defined($hash->{helper}{PHONEBOOK}{$number})); undef $number; } } undef $contact_name; } } undef $phonebook; $count_contacts = scalar keys %{$hash->{helper}{PHONEBOOK}}; Log3 $name, 2, "FB_CALLMONITOR: $name read ".($count_contacts > 0 ? $count_contacts : "no")." contact".($count_contacts == 1 ? "" : "s")." from FritzBox phonebook"; } else { Log3 $name, 2, "FB_CALLMONITOR: the file $phonebook_file is not a FritzBox phonebook"; } } else { Log3 $name, 2, "FB_CALLMONITOR: $name internal could not read FritzBox phonebook file: $phonebook_file"; } } } sub FB_CALLMONITOR_loadCacheFile($) { my ($hash) = @_; my $file = AttrVal($hash->{NAME}, "reverse-search-cache-file", ""); my @cachefile; my @tmpline; my $count_contacts; my $name = $hash->{NAME}; $file =~ s/(^\s+|\s+$)//g; if($file ne "") { delete($hash->{helper}{CACHE}) if(defined($hash->{helper}{CACHE})); Log3 $hash->{NAME}, 3, "FB_CALLMONITOR: loading cache file $file"; if(open(CACHEFILE, "$file")) { @cachefile = ; close(CACHEFILE); foreach my $line (@cachefile) { if(not $line =~ /^\s*$/) { $line =~ s/\n//g; @tmpline = split("\\|", $line); if(@tmpline == 2) { $hash->{helper}{CACHE}{$tmpline[0]} = $tmpline[1]; } } } $count_contacts = scalar keys %{$hash->{helper}{CACHE}}; Log3 $name, 2, "FB_CALLMONITOR: $name read ".($count_contacts > 0 ? $count_contacts : "no")." contact".($count_contacts == 1 ? "" : "s")." from Cache"; } else { Log3 $hash->{NAME}, 3, "FB_CALLMONITOR: could not open cache file"; } } } 1; =pod =begin html

FB_CALLMONITOR

=end html =begin html_DE

FB_CALLMONITOR

=end html_DE =cut