# $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 Digest::MD5; use HttpUtils; use DevIo; use FritzBoxUtils; 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) = @_; # 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->{NotifyFn} = "FB_CALLMONITOR_Notify"; $hash->{NOTIFYDEV} = "global"; $hash->{AttrList} = "do_not_notify:0,1 ". "disable:0,1 ". "unique-call-ids:0,1 ". "local-area-code ". "country-code ". "remove-leading-zero:0,1 ". "reverse-search-cache-file ". "reverse-search:multiple-strict,phonebook,klicktel.de,dasoertliche.de,search.ch,dasschnelle.at ". "reverse-search-cache:0,1 ". "reverse-search-phonebook-file ". "fritzbox-remote-phonebook:0,1 ". "fritzbox-remote-phonebook-via:web,telnet ". "fritzbox-remote-phonebook-exclude ". "fritzbox-remote-timeout ". "fritzbox-user ". $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 $dev = $a[2]; $dev .= ":1012" if($dev !~ m/:/ && $dev ne "none" && $dev !~ m/\@/); $hash->{DeviceName} = $dev; return DevIo_OpenDev($hash, 0, undef); } ##################################### # closing the connection on undefinition (shutdown/delete) sub FB_CALLMONITOR_Undef($$) { my ($hash, $arg) = @_; DevIo_CloseDev($hash); return undef; } ##################################### # Get function for returning a reverse search name sub FB_CALLMONITOR_Get($@) { my ($hash, @arguments) = @_; return "argument missing" if(int(@arguments) < 2); if($arguments[1] eq "search" and int(@arguments) == 3) { return FB_CALLMONITOR_reverseSearch($hash, $arguments[2]); } elsif($arguments[1] eq "showPhonebookIds" and exists($hash->{helper}{PHONEBOOK_NAMES})) { my $table = ""; my $head = "Id Name"; my $width = 10; foreach my $phonebookId (sort keys %{$hash->{helper}{PHONEBOOK_NAMES}}) { my $string = sprintf("%-3s", $phonebookId)." - ".$hash->{helper}{PHONEBOOK_NAMES}{$phonebookId}; $width = length($string) if(length($string) > $width); $table .= $string."\n"; } return $head."\n".("-" x $width)."\n".$table; } elsif($arguments[1] eq "showPhonebookEntries" and exists($hash->{helper}{PHONEBOOK})) { my $table = ""; my $number_width = 0; my $name_width = 0; foreach my $number (keys %{$hash->{helper}{PHONEBOOK}}) { $number_width = length($number) if($number_width < length($number)); $name_width = length($hash->{helper}{PHONEBOOK}{$number}) if($name_width < length($hash->{helper}{PHONEBOOK}{$number})); } my $head = sprintf("%-".$number_width."s %s" ,"Number", "Name"); foreach my $number (sort { lc($hash->{helper}{PHONEBOOK}{$a}) cmp lc($hash->{helper}{PHONEBOOK}{$b}) } keys %{$hash->{helper}{PHONEBOOK}}) { my $string = sprintf("%-".$number_width."s - %s" , $number,$hash->{helper}{PHONEBOOK}{$number}); $table .= $string."\n"; } return $head."\n".("-" x ($number_width + $name_width + 3))."\n".$table; } elsif($arguments[1] eq "showCacheEntries" and exists($hash->{helper}{CACHE})) { my $table = ""; my $number_width = 0; my $name_width = 0; foreach my $number (keys %{$hash->{helper}{CACHE}}) { $number_width = length($number) if($number_width < length($number)); $name_width = length($hash->{helper}{CACHE}{$number}) if($name_width < length($hash->{helper}{CACHE}{$number})); } my $head = sprintf("%-".$number_width."s %s" ,"Number", "Name"); foreach my $number (sort { lc($hash->{helper}{CACHE}{$a}) cmp lc($hash->{helper}{CACHE}{$b}) } keys %{$hash->{helper}{CACHE}}) { my $string = sprintf("%-".$number_width."s - %s" , $number,$hash->{helper}{CACHE}{$number}); $table .= $string."\n"; } return $head."\n".("-" x ($number_width + $name_width + 3))."\n".$table; } else { return "unknown argument ".$arguments[1].", choose one of search".(exists($hash->{helper}{PHONEBOOK_NAMES}) ? " showPhonebookIds" : "").(exists($hash->{helper}{PHONEBOOK}) ? " showPhonebookEntries" : "").(exists($hash->{helper}{CACHE}) ? " showCacheEntries" : ""); } } ##################################### # Set function for executing a reread of the internal phonebook sub FB_CALLMONITOR_Set($@) { my ($hash, @a) = @_; my $name = $hash->{NAME}; my $usage; my @sets = (); push @sets, "rereadPhonebook" if(defined($hash->{helper}{PHONEBOOK}) or AttrVal($name, "reverse-search" , "") =~ /(all|phonebook|internal)/); push @sets, "rereadCache" if(defined(AttrVal($name, "reverse-search-cache-file" , undef))); push @sets, "password" if($hash->{helper}{PWD_NEEDED}); $usage = "Unknown argument ".$a[1].", choose one of ".join(" ", @sets) if(scalar @sets > 0); if($a[1] eq "rereadPhonebook") { FB_CALLMONITOR_readPhonebook($hash); return undef; } elsif($a[1] eq "rereadCache") { FB_CALLMONITOR_loadCacheFile($hash); return undef; } elsif($a[1] eq "password") { return FB_CALLMONITOR_storePassword($hash, $a[2]) if($hash->{helper}{PWD_NEEDED}); Log3 $name, 2, "FB_CALLMONITOR ($name) - SOMEONE UNWANTED TRIED TO SET A NEW FRITZBOX PASSWORD!!!"; return "I didn't ask for a password, so go away!!!" } 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; foreach $data (split(/^/m, $buf)) { chomp $data; Log3 $name, 5, "FB_CALLMONITOR ($name) - received data: $data"; @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, "FB_CALLMONITOR ($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"); Log3 $name, 4, "FB_CALLMONITOR ($name) - reverse search returned: $reverse_search" if(defined($reverse_search)); if($array[1] eq "CALL" or $array[1] eq "RING") { delete($hash->{helper}{TEMP}{$array[2]}) if(exists($hash->{helper}{TEMP}{$array[2]})); if(AttrVal($name, "unique-call-ids", "0") eq "1") { $hash->{helper}{TEMP}{$array[2]}{call_id} = Digest::MD5::md5_hex($data); } } if($array[1] eq "CALL") { $hash->{helper}{TEMP}{$array[2]}{external_number} = (defined($external_number) ? $external_number : "unknown"); $hash->{helper}{TEMP}{$array[2]}{external_name} = (defined($reverse_search) ? $reverse_search : "unknown"); $hash->{helper}{TEMP}{$array[2]}{internal_number} = $array[4]; $hash->{helper}{TEMP}{$array[2]}{external_connection} = $array[6]; $hash->{helper}{TEMP}{$array[2]}{internal_connection} = $connection_type{$array[3]} if(defined($connection_type{$array[3]})); $hash->{helper}{TEMP}{$array[2]}{direction} = "outgoing"; } if($array[1] eq "RING") { $hash->{helper}{TEMP}{$array[2]}{external_number} = (defined($external_number) ? $external_number : "unknown"); $hash->{helper}{TEMP}{$array[2]}{external_name} = (defined($reverse_search) ? $reverse_search : "unknown"); $hash->{helper}{TEMP}{$array[2]}{internal_number} = $array[4]; $hash->{helper}{TEMP}{$array[2]}{external_connection} = $array[5]; $hash->{helper}{TEMP}{$array[2]}{direction} = "incoming"; } if($array[1] eq "CONNECT" and not exists($hash->{helper}{TEMP}{$array[2]}{internal_connection})) { $hash->{helper}{TEMP}{$array[2]}{internal_connection} = $connection_type{$array[3]} if(defined($connection_type{$array[3]})); } if($array[1] eq "DISCONNECT") { $hash->{helper}{TEMP}{$array[2]}{call_duration} = $array[3]; if(exists($hash->{helper}{TEMP}{$array[2]}{direction}) and exists($hash->{helper}{TEMP}{$array[2]}{external_number}) and $hash->{helper}{TEMP}{$array[2]}{direction} eq "incoming" and $array[3] eq "0") { $hash->{helper}{TEMP}{$array[2]}{missed_call} = $hash->{helper}{TEMP}{$array[2]}{external_number}.(exists($hash->{helper}{TEMP}{$array[2]}{external_name}) and $hash->{helper}{TEMP}{$array[2]}{external_name} ne "unknown" ? " (".$hash->{helper}{TEMP}{$array[2]}{external_name}.")" : ""); $hash->{helper}{TEMP}{$array[2]}{missed_call_line} = $hash->{helper}{TEMP}{$array[2]}{internal_number}; } } readingsBeginUpdate($hash); readingsBulkUpdate($hash, "event", lc($array[1])); foreach my $key (keys %{$hash->{helper}{TEMP}{$array[2]}}) { readingsBulkUpdate($hash, $key, $hash->{helper}{TEMP}{$array[2]}{$key}) unless($key eq "call_id"); } if(AttrVal($name, "unique-call-ids", "0") eq "1" and exists($hash->{helper}{TEMP}{$array[2]}{call_id})) { readingsBulkUpdate($hash, "call_id", $hash->{helper}{TEMP}{$array[2]}{call_id}); } else { readingsBulkUpdate($hash, "call_id", $array[2]); } if($array[1] eq "DISCONNECT") { delete($hash->{helper}{TEMP}{$array[2]}) if(exists($hash->{helper}{TEMP}{$array[2]})); } readingsEndUpdate($hash, 1); } } ##################################### # Reconnects to FritzBox in case of disconnects sub FB_CALLMONITOR_Ready($) { my ($hash) = @_; return DevIo_OpenDev($hash, 1, undef); } ##################################### # Handles Attribute Changes sub FB_CALLMONITOR_Attr($@) { my ($cmd, $name, $attrib, $value) = @_; my $hash = $defs{$name}; if($cmd eq "set") { if((($attrib eq "reverse-search" and $value =~ /(all|internal|phonebook)/) or $attrib eq "reverse-search-phonebook-file" or $attrib eq "fritzbox-remote-phonebook") and $init_done == 1) { $attr{$name}{$attrib} = $value; return FB_CALLMONITOR_readPhonebook($hash); } if($attrib eq "reverse-search-cache-file") { return FB_CALLMONITOR_loadCacheFile($hash, $value); } if($attrib eq "disable") { if($value eq "0") { DevIo_OpenDev($hash, 0, undef); } elsif($value eq "1") { DevIo_CloseDev($hash); $hash->{STATE} = "disabled"; } } } elsif($cmd eq "del") { if($attrib eq "reverse-search" or $attrib eq "reverse-search-phonebook-file") { delete($hash->{helper}{PHONEBOOK}) if(defined($hash->{helper}{PHONEBOOK})); } if($attrib eq "reverse-search-cache") { delete($hash->{helper}{CACHE}) if(defined($hash->{helper}{CACHE})); } if($attrib eq "disable") { DevIo_OpenDev($hash, 0, undef); } } return undef; } sub FB_CALLMONITOR_Notify($$) { my ($hash,$dev) = @_; return if($dev->{NAME} ne "global"); return if(!grep(m/^INITIALIZED|REREADCFG$/, @{$dev->{CHANGED}})); FB_CALLMONITOR_readPhonebook($hash); } ############################################################################################################ # # Begin of helper functions # ############################################################################################################ ##################################### # Performs a reverse search of a phone number sub FB_CALLMONITOR_reverseSearch($$) { my ($hash, $number) = @_; my $name = $hash->{NAME}; my $result; my $invert_match = undef; my @attr_list = split("(,|\\|)", AttrVal($name, "reverse-search", "none")); chomp $number; # Using Cache if enabled if(AttrVal($name, "reverse-search-cache", "0") eq "1" and 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" or $hash->{helper}{CACHE}{$number} ne "unknown") { return $hash->{helper}{CACHE}{$number}; } } # Using internal phonebook if available and enabled if((grep { /^(all|phonebook|internal)$/ } @attr_list) 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" and defined($hash->{helper}{CACHE}{$number})) { return $hash->{helper}{CACHE}{$number} if($hash->{helper}{CACHE}{$number} ne "timeout"); } # Ask klicktel.de if((grep { /^(all|klicktel\.de)$/ } @attr_list)) { Log3 $name, 4, "FB_CALLMONITOR ($name) - using klicktel.de for reverse search of $number"; $result = GetFileFromURL("http://openapi.klicktel.de/searchapi/invers?key=0de6139a49055c37b9b2d7bb3933cb7b&number=".$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 =~ /"displayname":"([^"]*?)"/) { $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(grep { /^(all|dasoertliche\.de)$/ } @attr_list) { 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 =~ /(.+?)<\/span>/) { $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; } elsif(not $result =~ /wir konnten keine Treffer finden/) { Log3 $name, 3, "FB_CALLMONITOR ($name) - the reverse search result for $number could not be extracted from dasoertliche.de. Please contact the FHEM community."; } } } # SWITZERLAND ONLY!!! Ask search.ch if(grep { /^search\.ch$/ } @attr_list) { Log3 $name, 4, "FB_CALLMONITOR ($name) - using search.ch for reverse search of $number"; $result = GetFileFromURL("http://tel.search.ch/api/?key=b0b1207cb7c9d0048867de887aa9a4fd&maxnum=1&was=".$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 =~ /(.+?)<\/entry>/s) { my $xml = $1; $invert_match = ""; if($xml =~ /(.+?)<\/tel:firstname>/) { $invert_match .= $1; } if($xml =~ /(.+?)<\/tel:name>/) { $invert_match .= " $1"; } if($xml =~ /(.+?)<\/tel:occupation>/) { $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(grep { /^dasschnelle\.at$/ } @attr_list) { 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; } elsif(not $result =~ /Es wurden keine passenden Eintr.ge gefunden/) { Log3 $name, 3, "FB_CALLMONITOR ($name) - the reverse search result for $number could not be extracted from dasschnelle.at. Please contact the FHEM community."; } } } 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; } ##################################### # replaces all HTML entities to their utf-8 counter parts. sub FB_CALLMONITOR_html2txt($) { my ($string) = @_; $string =~ s/ / /g; $string =~ s/&/&/g; $string =~ s/(\xe4|ä|\\u00e4|\\u00E4)/ä/g; $string =~ s/(\xc4|Ä|\\u00c4|\\u00C4)/Ä/g; $string =~ s/(\xf6|ö|\\u00f6|\\u00F6)/ö/g; $string =~ s/(\xd6|Ö|\\u00d6|\\u00D6)/Ö/g; $string =~ s/(\xfc|ü|\\u00fc|\\u00FC)/ü/g; $string =~ s/(\xdc|Ü|\\u00dc|\\u00DC)/Ü/g; $string =~ s/(\xdf|ß)/ß/g; $string =~ s/<.+?>//g; $string =~ s/(^\s+|\s+$)//g; return trim($string); } ##################################### # writes reverse search result to the cache and if enabled to the cache file sub FB_CALLMONITOR_writeToCache($$$) { my ($hash, $number, $txt) = @_; my $name = $hash->{NAME}; my $file = AttrVal($name, "reverse-search-cache-file", ""); my $err; my @cachefile; my $phonebook_file; $file =~ s/(^\s+|\s+$)//g; $hash->{helper}{CACHE}{$number} = $txt; if($file ne "") { Log3 $name, 4, "FB_CALLMONITOR ($name) - opening cache file $file for writing $number ($txt)"; foreach my $key (keys %{$hash->{helper}{CACHE}}) { push @cachefile, "$key|".$hash->{helper}{CACHE}{$key}; } $err = FileWrite($file,@cachefile); if(defined($err) && $err) { Log3 $name, 2, "FB_CALLMONITOR ($name) - could not write cache file: $err"; } } } ##################################### # reads a FritzBox phonebook sub FB_CALLMONITOR_readPhonebook($;$) { my ($hash, $testPassword) = @_; my $name = $hash->{NAME}; my ($err, $count_contacts, @lines, $phonebook); if(AttrVal($name, "fritzbox-remote-phonebook", "0") eq "1") { if(AttrVal($name, "fritzbox-remote-phonebook-via", "web") eq "telnet") { ($err, $phonebook) = FB_CALLMONITOR_readRemotePhonebookViaTelnet($hash, $testPassword); if(defined($err)) { Log3 $name, 2, "FB_CALLMONITOR ($name) - could not read remote FritzBox phonebook file - $err"; return "Could not read remote FritzBox phonebook file - $err"; } Log3 $name, 2, "FB_CALLMONITOR ($name) - found remote FritzBox phonebook via telnet"; ($err, $count_contacts) = FB_CALLMONITOR_parsePhonebook($hash, $phonebook); if(defined($err)) { Log3 $name, 2, "FB_CALLMONITOR ($name) - could not parse remote phonebook - $err"; return "Could not parse remote phonebook - $err"; } else { Log3 $name, 2, "FB_CALLMONITOR ($name) - read $count_contacts contact".($count_contacts == 1 ? "" : "s")." from remote phonebook via telnet"; } } elsif(AttrVal($name, "fritzbox-remote-phonebook-via", "web") eq "web") { $err = FB_CALLMONITOR_identifyPhoneBooksViaWeb($hash, $testPassword); if(defined($err)) { Log3 $name, 2, "FB_CALLMONITOR ($name) - could not identify remote phonebooks - $err"; return "could not identify remote phonebooks - $err"; } unless(exists($hash->{helper}{PHONEBOOK_NAMES})) { Log3 $name, 2, "FB_CALLMONITOR ($name) - no phonebooks could be found"; return "no phonebooks could be found"; } my %excludedIds = map { trim($_) => 1 } split(",",AttrVal($name, "fritzbox-remote-phonebook-exclude", "")); foreach my $phonebookId (sort keys %{$hash->{helper}{PHONEBOOK_NAMES}}) { if(exists($excludedIds{$phonebookId})) { Log3 $name, 4, "FB_CALLMONITOR ($name) - skipping excluded phonebook id $phonebookId (".$hash->{helper}{PHONEBOOK_NAMES}{$phonebookId}.")"; next; } Log3 $name, 4, "FB_CALLMONITOR ($name) - requesting phonebook id $phonebookId (".$hash->{helper}{PHONEBOOK_NAMES}{$phonebookId}.")"; ($err, $phonebook) = FB_CALLMONITOR_readRemotePhonebookViaWeb($hash, $phonebookId, $testPassword); if(defined($err)) { Log3 $name, 2, "FB_CALLMONITOR ($name) - unable to retrieve phonebook \"".$hash->{helper}{PHONEBOOK_NAMES}{$phonebookId}."\" from FritzBox - $err"; return "unable to retrieve phonebook \"".$hash->{helper}{PHONEBOOK_NAMES}{$phonebookId}."\" from FritzBox - $err"; } else { ($err, $count_contacts) = FB_CALLMONITOR_parsePhonebook($hash, $phonebook); if(defined($err)) { Log3 $name, 2, "FB_CALLMONITOR ($name) - could not parse remote phonebook ".$hash->{helper}{PHONEBOOK_NAMES}{$phonebookId}." - $err"; return "could not parse remote phonebook ".$hash->{helper}{PHONEBOOK_NAMES}{$phonebookId}." - $err"; } else { Log3 $name, 2, "FB_CALLMONITOR ($name) - read $count_contacts contact".($count_contacts == 1 ? "" : "s")." from remote phonebook \"".$hash->{helper}{PHONEBOOK_NAMES}{$phonebookId}."\""; } } } } } else { Log3 $name, 4, "FB_CALLMONITOR ($name) - skipping remote phonebook"; } if(-e "/usr/bin/ctlmgr_ctl" or ((not -e "/usr/bin/ctlmgr_ctl") and defined(AttrVal($name, "reverse-search-phonebook-file", undef)))) { my $phonebook_file = AttrVal($name, "reverse-search-phonebook-file", "/var/flash/phonebook"); ($err, @lines) = FileRead($phonebook_file); if(defined($err) && $err) { Log3 $name, 2, "FB_CALLMONITOR ($name) - could not read FritzBox phonebook file - $err"; return "Could not read FritzBox phonebook file - $err"; } $phonebook = join("", @lines); Log3 $name, 2, "FB_CALLMONITOR ($name) - found FritzBox phonebook $phonebook_file"; ($err, $count_contacts) = FB_CALLMONITOR_parsePhonebook($hash, $phonebook); if(defined($err)) { Log3 $name, 2, "FB_CALLMONITOR ($name) - could not parse $phonebook_file - $err"; return "Could not parse $phonebook_file - $err"; } else { Log3 $name, 2, "FB_CALLMONITOR ($name) - read $count_contacts contact".($count_contacts == 1 ? "" : "s")." from $phonebook_file"; } } else { Log3 $name, 4, "FB_CALLMONITOR ($name) - skipping local phonebook file"; } } ##################################### # reads the FritzBox phonebook file and parses the entries sub FB_CALLMONITOR_parsePhonebook($$) { my ($hash, $phonebook) = @_; my $name = $hash->{NAME}; my $contact; my $contact_name; my $number; my $count_contacts = 0; my $area_code = AttrVal($name, "local-area-code", ""); my $country_code = AttrVal($name, "country-code", "0049"); if($phonebook =~ // and $phonebook =~ //) { while($phonebook =~ m/]*>(.+?)<\/contact>/gs) { $contact = $1; if($contact =~ m/(.+?)<\/realName>/) { $contact_name = $1; while($contact =~ m/]*?type="([^<>"]+?)"[^<>]*?>([^<>"]+?)<\/number>/gs) { if($1 ne "intern" and $1 ne "memo") { $number = $2; $number =~ s/\s//g; # Remove spaces $number =~ s/^\+/00/g; # Convert leading + to 00 country extension $number =~ s/^$country_code/0/g; # Replace own country code with leading 0 $number =~ s/^(\#[0-9]{1,10}\#)//g; # Remove phone control codes $number =~ s/[^*\d]//g if(not $number =~ /@/); # Remove anything else isn't a number if it is no VoIP number if(not $number =~ /^0/ and not $number =~ /@/ and $area_code =~ /^0[1-9]\d+$/) { $number = $area_code.$number; } $count_contacts++; Log3 $name, 4, "FB_CALLMONITOR ($name) - found $contact_name with number $number"; $hash->{helper}{PHONEBOOK}{$number} = FB_CALLMONITOR_html2txt($contact_name) if(not defined($hash->{helper}{PHONEBOOK}{$number})); undef $number; } } undef $contact_name; } } undef $phonebook; return (undef, $count_contacts); } else { return "this is not a FritzBox phonebook"; } } ##################################### # loads the reverse search cache from file sub FB_CALLMONITOR_loadCacheFile($;$) { my ($hash, $file) = @_; my @cachefile; my @tmpline; my $count_contacts; my $name = $hash->{NAME}; my $err; $file = AttrVal($hash->{NAME}, "reverse-search-cache-file", "") unless(defined($file)); $file =~ s/(^\s+|\s+$)//g; if($file ne "" and -r $file) { delete($hash->{helper}{CACHE}) if(defined($hash->{helper}{CACHE})); Log3 $hash->{NAME}, 3, "FB_CALLMONITOR ($name) - loading cache file $file"; ($err, @cachefile) = FileRead($file); unless(defined($err) and $err) { 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 $name, 3, "FB_CALLMONITOR ($name) - could not open cache file: $err"; } } else { Log3 $name, 3, "FB_CALLMONITOR ($name) - unable to access cache file: $file"; } } ##################################### # loads phonebook from extern FritzBox sub FB_CALLMONITOR_readRemotePhonebookViaTelnet($;$) { my ($hash, $testPassword) = @_; my $name = $hash->{NAME}; my $rc = eval { require Net::Telnet; Net::Telnet->import(); 1; }; unless($rc) { return "Error loading Net::Telnet. May be this module is not installed."; } my $phonebook_file = "/var/flash/phonebook"; my ($fb_ip,undef) = split(/:/, ($hash->{DeviceName}), 2); $hash->{helper}{READ_PWD} = 1; my $fb_user = AttrVal($name, "fritzbox-user", undef); my $fb_pw = FB_CALLMONITOR_readPassword($hash, $testPassword); delete($hash->{helper}{READ_PWD}) if(exists($hash->{helper}{READ_PWD})); return "no password available to access FritzBox" unless(defined($fb_pw)); my $telnet = new Net::Telnet ( Timeout=>10, Errmode=>'return'); unless($telnet->open($fb_ip)) { return "Error Connecting to FritzBox: ".$telnet->errmsg; } Log3 $name, 4, "FB_CALLMONITOR ($name) - connected to FritzBox via telnet"; my ($prematch, $match) = $telnet->waitfor('/(?:login|user|password):\s*$/i'); unless(defined($prematch) and defined($match)) { $telnet->close; return "Couldn't recognize login prompt: ".$telnet->errmsg; } if($match =~ /(login|user):/i and defined($fb_user)) { Log3 $name, 4, "FB_CALLMONITOR ($name) - setting user to FritzBox: $fb_user"; $telnet->print($fb_user); unless($telnet->waitfor('/password:\s*$/i')) { $telnet->close; return "Error giving password to FritzBox: ".$telnet->errmsg; } Log3 $name, 4, "FB_CALLMONITOR ($name) - giving password to FritzBox"; $telnet->print($fb_pw); } elsif($match =~ /(login|user):/i and not defined($fb_user)) { $telnet->close; return "FritzBox needs a username to login via telnet. Please provide a valid username/password combination"; } elsif($match =~ /password:/i) { Log3 $name, 4, "FB_CALLMONITOR ($name) - giving password to FritzBox"; $telnet->print($fb_pw); } unless($telnet->waitfor('/#\s*$/')) { $telnet->close; my $err = $telnet->errmsg; $hash->{helper}{PWD_NEEDED} = 1; return "wrong password, please provide the right password" if($err =~ /\s*eof/i); return "Could'nt recognize shell prompt: $err"; } Log3 $name, 4, "FB_CALLMONITOR ($name) - requesting $phonebook_file via telnet"; my $command = "cat ".$phonebook_file; my @FBPhoneBook = $telnet->cmd($command); unless(@FBPhoneBook) { $telnet->close; return "Error getting phonebook FritzBox: ".$telnet->errmsg; } else { Log3 $name, 3, "FB_CALLMONITOR ($name) - Getting phonebook from FritzBox: $phonebook_file"; } $telnet->print('exit'); $telnet->close; delete($hash->{helper}{PWD_NEEDED}) if(exists($hash->{helper}{PWD_NEEDED})); return (undef, join('', @FBPhoneBook)); } ##################################### # identifys the phonebooks defined on the FritzBox via web interface (http) sub FB_CALLMONITOR_identifyPhoneBooksViaWeb($;$) { my ($hash, $testPassword) = @_; my $name = $hash->{NAME}; my ($fb_ip,undef) = split(/:/, ($hash->{DeviceName}), 2); my $fb_user = AttrVal($name, "fritzbox-user", undef); my $fb_pw; my $fb_sid; $hash->{helper}{READ_PWD} = 1; $fb_pw = FB_CALLMONITOR_readPassword($hash, $testPassword); delete($hash->{helper}{READ_PWD}) if(exists($hash->{helper}{READ_PWD})); return "no password available to access FritzBox" unless(defined($fb_pw)); $fb_sid = FB_doCheckPW($fb_ip, $fb_user, $fb_pw); unless(defined($fb_sid)) { $hash->{helper}{PWD_NEEDED} = 1; return "unable to login via webinterface, maybe wrong user/password?" } Log3 $name, 4, "FB_CALLMONITOR ($name) - identifying available phonebooks"; my $param; $param->{url} = "http://$fb_ip/fon_num/fonbook_select.lua?sid=$fb_sid"; $param->{noshutdown} = 1; $param->{timeout} = AttrVal($name, "fritzbox-remote-timeout", 5); $param->{loglevel} = 4; my ($err, $data) = HttpUtils_BlockingGet($param); if ($err ne "") { Log3 $name, 3, "FB_CALLMONITOR ($name) - error while requesting phonebooks: $err"; return "error while requesting phonebooks: $err"; } if($data eq "" and exists($param->{code})) { Log3 $name, 3, "FB_CALLMONITOR ($name) - received http code ".$param->{code}." without any data after requesting available phonebooks"; return "received no data after requesting available phonebooks"; } Log3 $name, 4, "FB_CALLMONITOR ($name) - phonebooks successfully identified"; if($data =~ m/]*name="mainform"[^>]*>(.+?)<\/form>/s) { $data = $1; } delete($hash->{helper}{PHONEBOOK_NAMES}) if(exists($hash->{helper}{PHONEBOOK_NAMES})); while($data =~ /]*for="uiBookid:(\d+)"[^>]*>\s*(.+?)\s*<\/label>/gcs) { $hash->{helper}{PHONEBOOK_NAMES}{$1} = $2; Log3 $name, 4, "FB_CALLMONITOR ($name) - found phonebook: $2"; } Log3 $name, 3, "FB_CALLMONITOR ($name) - phonebooks found: ".join(", ", map { $hash->{helper}{PHONEBOOK_NAMES}{$_}." (id: $_)" } sort keys %{$hash->{helper}{PHONEBOOK_NAMES}}) if(exists($hash->{helper}{PHONEBOOK_NAMES})); delete($hash->{helper}{PWD_NEEDED}) if(exists($hash->{helper}{PWD_NEEDED})); return undef; } ##################################### # loads internal and online phonebooks from extern FritzBox via web interface (http) sub FB_CALLMONITOR_readRemotePhonebookViaWeb($$;$) { my ($hash, $phonebookId, $testPassword) = @_; my $name = $hash->{NAME}; my ($fb_ip,undef) = split(/:/, ($hash->{DeviceName}), 2); my $fb_user = AttrVal($name, "fritzbox-user", ''); my $fb_pw; my $fb_sid; return "unknown phonebook id: $phonebookId" unless(exists($hash->{helper}{PHONEBOOK_NAMES}{$phonebookId})); $hash->{helper}{READ_PWD} = 1; $fb_pw = FB_CALLMONITOR_readPassword($hash, $testPassword); delete($hash->{helper}{READ_PWD}) if(exists($hash->{helper}{READ_PWD})); return "no password available to access FritzBox" unless(defined($fb_pw)); $fb_sid = FB_doCheckPW($fb_ip, $fb_user, $fb_pw); unless(defined($fb_sid)) { $hash->{helper}{PWD_NEEDED} = 1; return "no session id available to access FritzBox. Maybe wrong user/password?" } my $param; $param->{url} = "http://$fb_ip/cgi-bin/firmwarecfg"; $param->{noshutdown} = 1; $param->{timeout} = AttrVal($name, "fritzbox-remote-timeout", 5); $param->{loglevel} = 4; $param->{method} = "POST"; $param->{header} = "Content-Type: multipart/form-data; boundary=boundary"; $param->{data} = "--boundary\r\n". "Content-Disposition: form-data; name=\"sid\"\r\n". "\r\n". "$fb_sid\r\n". "--boundary\r\n". "Content-Disposition: form-data; name=\"PhonebookId\"\r\n". "\r\n". "$phonebookId\r\n". "--boundary\r\n". "Content-Disposition: form-data; name=\"PhonebookExportName\"\r\n". "\r\n". $hash->{helper}{PHONEBOOK_NAMES}{$phonebookId}."\r\n". "--boundary\r\n". "Content-Disposition: form-data; name=\"PhonebookExport\"\r\n". "\r\n". "\r\n". "--boundary--"; Log3 $name, 4, "FB_CALLMONITOR ($name) - get export for phonebook: $phonebookId"; my ($err, $phonebook) = HttpUtils_BlockingGet($param); Log3 $name, 5, "FB_CALLMONITOR ($name) - received http response code ".$param->{code} if(exists($param->{code})); if ($err ne "") { Log3 $name, 3, "FB_CALLMONITOR ($name) - got error while requesting phonebook: $err"; return "got error while requesting phonebook: $err"; } if($phonebook eq "" and exists($param->{code})) { Log3 $name, 3, "FB_CALLMONITOR ($name) - received http code ".$param->{code}." without any data"; return "received http code ".$param->{code}." without any data"; } delete($hash->{helper}{PWD_NEEDED}) if(exists($hash->{helper}{PWD_NEEDED})); return (undef, $phonebook); } ##################################### # checks and store FritzBox password used for telnet connection sub FB_CALLMONITOR_storePassword($$) { my ($hash, $password) = @_; my $index = $hash->{TYPE}."_".$hash->{NAME}."_passwd"; my $key = getUniqueId().$index; my $enc_pwd = ""; if(eval "use Digest::MD5;1") { $key = Digest::MD5::md5_hex(unpack "H*", $key); $key .= Digest::MD5::md5_hex($key); } for my $char (split //, $password) { my $encode=chop($key); $enc_pwd.=sprintf("%.2x",ord($char)^ord($encode)); $key=$encode.$key; } my $err = FB_CALLMONITOR_readPhonebook($hash, $enc_pwd); return "unable to check password - $err" if(defined($err)); $err = setKeyValue($index, $enc_pwd); return "error while saving the password - $err" if(defined($err)); return "password successfully saved"; } ##################################### # reads the FritzBox password sub FB_CALLMONITOR_readPassword($;$) { my ($hash, $testPassword) = @_; my $name = $hash->{NAME}; my $index = $hash->{TYPE}."_".$hash->{NAME}."_passwd"; my $key = getUniqueId().$index; my ($password, $err); if(defined($testPassword)) { $password = $testPassword; } else { ($err, $password) = getKeyValue($index); if(defined($err)) { $hash->{helper}{PWD_NEEDED} = 1; Log3 $name, 4, "FB_CALLMONITOR ($name) - unable to read FritzBox password from file: $err"; return undef; } } if(defined($password)) { if(eval "use Digest::MD5;1") { $key = Digest::MD5::md5_hex(unpack "H*", $key); $key .= Digest::MD5::md5_hex($key); } my $dec_pwd = ''; for my $char (map { pack('C', hex($_)) } ($password =~ /(..)/g)) { my $decode=chop($key); $dec_pwd.=chr(ord($char)^ord($decode)); $key=$decode.$key; } return $dec_pwd if($hash->{helper}{READ_PWD}); } else { $hash->{helper}{PWD_NEEDED} = 1; return undef; } } 1; =pod =begin html

FB_CALLMONITOR

=end html =begin html_DE

FB_CALLMONITOR

=end html_DE =cut