# $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 Encode qw(encode); use Digest::MD5; use HttpUtils; use DevIo; use FritzBoxUtils; ##################################### 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->{RenameFn} = "FB_CALLMONITOR_Rename"; $hash->{DeleteFn} = "FB_CALLMONITOR_Delete"; $hash->{UndefFn} = "FB_CALLMONITOR_Undef"; $hash->{AttrFn} = "FB_CALLMONITOR_Attr"; $hash->{NotifyFn} = "FB_CALLMONITOR_Notify"; $hash->{AttrList} = "do_not_notify:0,1 ". "disable:0,1 ". "disabledForIntervals ". "unique-call-ids:0,1 ". "local-area-code ". "country-code ". "remove-leading-zero:0,1 ". "answMachine-is-missed-call:0,1 ". "check-deflections:0,1 ". "reverse-search-cache-file ". "reverse-search:sortable-strict,phonebook,textfile,dasoertliche.de,11880.com,search.ch,dasschnelle.at,herold.at ". "reverse-search-cache:0,1 ". "reverse-search-phonebook-file ". "reverse-search-text-file ". "fritzbox-remote-phonebook:0,1 ". "fritzbox-remote-phonebook-via:web,tr064,telnet ". "fritzbox-remote-phonebook-exclude ". "fritzbox-remote-timeout ". "fritzbox-user ". "apiKeySearchCh ". "sendKeepAlives:none,5m,10m,15m,30m,1h ". "contactImageViaTR064:0,1 ". "contactImageDirectory ". "contactDefaultImage ". $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; } $hash->{NOTIFYDEV} = "global"; DevIo_CloseDev($hash); delete($hash->{NEXT_OPEN}); my $dev = $a[2]; $dev .= ":1012" if($dev !~ m/:/ && $dev ne "none" && $dev !~ m/\@/); $hash->{DeviceName} = $dev; return DevIo_OpenDev($hash, 0, undef, \&FB_CALLMONITOR_DevIoCallback) } ##################################### # closing the connection on undefinition (shutdown/delete) sub FB_CALLMONITOR_Undef($$) { my ($hash, $arg) = @_; RemoveInternalTimer($hash, "FB_CALLMONITOR_sendKeepAlive"); DevIo_CloseDev($hash); return undef; } ##################################### # If Device is deleted, delete the password dataIf Device is renamed, copy the password data sub FB_CALLMONITOR_Delete($$) { my ($hash, $name) = @_; my $index = "FB_CALLMONITOR_".$name."_passwd"; setKeyValue($index, undef); return undef; } ##################################### # If Device is renamed, copy the password data sub FB_CALLMONITOR_Rename($$) { my ($new, $old) = @_; my $old_index = "FB_CALLMONITOR_".$old."_passwd"; my $new_index = "FB_CALLMONITOR_".$new."_passwd"; my $old_key =getUniqueId().$old_index; my $new_key =getUniqueId().$new_index; my ($err, $old_pwd) = getKeyValue($old_index); return undef unless(defined($old_pwd)); setKeyValue($new_index, FB_CALLMONITOR_encrypt(FB_CALLMONITOR_decrypt($old_pwd,$old_key), $new_key)); setKeyValue($old_index, 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) { my $number = FB_CALLMONITOR_normalizePhoneNumber($hash, join '', @arguments[2..$#arguments]); my $result = FB_CALLMONITOR_reverseSearch($hash, $number); return $result if(defined($result)); return "no reverse search result found for $number"; } 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}) or exists($hash->{helper}{PHONEBOOKS})) and int(@arguments) <= 3) { return "given argument is not a valid phonebook id: ".$arguments[2] if(int(@arguments) == 3 and ($arguments[2] !~ /^\d+$/ or !exists($hash->{helper}{PHONEBOOKS}{$arguments[2]}) ) ); my $return = ""; if(exists($hash->{helper}{PHONEBOOKS})) { foreach my $pb_id (sort keys %{$hash->{helper}{PHONEBOOKS}}) { next if(int(@arguments) == 3 and int($arguments[2]) != $pb_id); my $number_width = 0; my $name_width = 0; my $table = ""; if(defined($hash->{helper}{PHONEBOOKS}{$pb_id}) and scalar(keys(%{$hash->{helper}{PHONEBOOKS}{$pb_id}})) > 0) { foreach my $number (keys %{$hash->{helper}{PHONEBOOKS}{$pb_id}}) { $number_width = length($number) if($number_width < length($number)); $name_width = length($hash->{helper}{PHONEBOOKS}{$pb_id}{$number}) if($name_width < length($hash->{helper}{PHONEBOOKS}{$pb_id}{$number})); } my $head = "Phonebook: ".$hash->{helper}{PHONEBOOK_NAMES}{$pb_id}." / Id: $pb_id\n\n ".sprintf("%-".$number_width."s %s" ,"Number", "Name"); foreach my $number (sort { lc($hash->{helper}{PHONEBOOKS}{$pb_id}{$a}) cmp lc($hash->{helper}{PHONEBOOKS}{$pb_id}{$b}) } keys %{$hash->{helper}{PHONEBOOKS}{$pb_id}}) { my $string = sprintf(" %-".$number_width."s - %s" , $number,$hash->{helper}{PHONEBOOKS}{$pb_id}{$number}); $table .= $string."\n"; } $return .= $head."\n ".("-" x ($number_width + $name_width + 3))."\n".$table."\n"; } } } if(exists($hash->{helper}{PHONEBOOK}) and int(@arguments) == 2) { my $number_width = 0; my $name_width = 0; my $table = ""; 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("Phonebook file\n\n %-".$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 } return $return; } 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; } elsif($arguments[1] eq "showTextfileEntries" and exists($hash->{helper}{TEXTFILE})) { my $table = ""; my $number_width = 0; my $name_width = 0; foreach my $number (keys %{$hash->{helper}{TEXTFILE}}) { $number_width = length($number) if($number_width < length($number)); $name_width = length($hash->{helper}{TEXTFILE}{$number}) if($name_width < length($hash->{helper}{TEXTFILE}{$number})); } my $head = sprintf("%-".$number_width."s %s" ,"Number", "Name"); foreach my $number (sort { lc($hash->{helper}{TEXTFILE}{$a}) cmp lc($hash->{helper}{TEXTFILE}{$b}) } keys %{$hash->{helper}{TEXTFILE}}) { my $string = sprintf("%-".$number_width."s - %s" , $number,$hash->{helper}{TEXTFILE}{$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}) or exists($hash->{helper}{PHONEBOOKS})) ? " showPhonebookEntries" : ""). (exists($hash->{helper}{CACHE}) ? " showCacheEntries" : ""). (exists($hash->{helper}{TEXTFILE}) ? " showTextfileEntries" : ""); } } ##################################### # 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, "rereadTextfile" if(defined(AttrVal($name, "reverse-search-text-file" , undef))); push @sets, "password" if($hash->{helper}{PWD_NEEDED}); push @sets, "reopen" if($hash->{FD}); $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 "rereadTextfile") { FB_CALLMONITOR_loadTextFile($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!!!" } elsif($a[1] eq "reopen") { DevIo_CloseDev($hash); DevIo_OpenDev($hash, 0, undef, \&FB_CALLMONITOR_DevIoCallback); return undef; } else { return $usage; } } ##################################### # Receives an event and creates several readings for event triggering sub FB_CALLMONITOR_Read($) { my ($hash) = @_; my %connection_type = ( 0 => "FON1", 1 => "FON2", 2 => "FON3", 3 => "Callthrough", 4 => "ISDN", 5 => "FAX", 6 => "Answering_Machine", 10 => "DECT_1", 11 => "DECT_2", 12 => "DECT_3", 13 => "DECT_4", 14 => "DECT_5", 15 => "DECT_6", 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", 36 => "ISDN_data", 37 => "FAX_data", 40 => "Answering_Machine_1", 41 => "Answering_Machine_2", 42 => "Answering_Machine_3", 43 => "Answering_Machine_4", 44 => "Answering_Machine_5" ); my $received = DevIo_SimpleRead($hash); my $buffer = $hash->{PARTIAL}; return "" if(!defined($received) or IsDisabled($hash->{NAME})); my $name = $hash->{NAME}; my @array; my $area_code = AttrVal($name, "local-area-code", ""); my $country_code = AttrVal($name, "country-code", "0049"); $buffer .= $received; while($buffer =~ m/\n/) { my $data; ($data, $buffer) = split("\n", $buffer, 2); chomp $data; my $external_number = undef; my $reverse_search = undef; my $is_deflected = undef; 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 ""); $is_deflected = FB_CALLMONITOR_checkNumberForDeflection($hash, $external_number) if($array[1] eq "RING"); if(defined($external_number)) { $external_number =~ s/^0// if(AttrVal($name, "remove-leading-zero", "0") eq "1"); if($array[1] eq "CALL") { # Remove Call-By-Call number (Germany) $external_number =~ s/^(010[1-9]\d|0100[1-9]\d)//g if($country_code eq "0049"); # Remove Call-By-Call number (Austria) $external_number =~ s/^10\d\d//g if($country_code eq "0043"); # Remove Call-By-Call number (Swiss) $external_number =~ s/^(107\d\d|108\d\d)//g if($country_code eq "0041"); } if($external_number =~ /^\d/ and $external_number !~ /^0/ and $external_number !~ /^11/ and $area_code ne "") { if($area_code =~ /^0[1-9]\d+$/ and $external_number =~ /^[1-9].+$/) { $external_number = $area_code.$external_number; } elsif($area_code !~ /^0[1-9]\d+$/) { 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($external_number !~ /^\*/); # Forum #85761 $reverse_search = FB_CALLMONITOR_reverseSearch($hash, $external_number) if(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] =~ /^CALL|RING$/) { delete($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); } else { $hash->{helper}{TEMP}{$array[2]}{call_id} = $array[2]; } $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]; if(my $contact_image = FB_CALLMONITOR_getContactImage($hash, $hash->{helper}{TEMP}{$array[2]}{external_number})) { $hash->{helper}{TEMP}{$array[2]}{contact_image} = $contact_image; } } if($array[1] eq "CALL") { $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_connection} = $array[5]; $hash->{helper}{TEMP}{$array[2]}{direction} = "incoming"; $hash->{helper}{TEMP}{$array[2]}{".deflected"} = $is_deflected; } 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]})); $hash->{helper}{TEMP}{$array[2]}{".internal_connection_id"} = $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 $hash->{helper}{TEMP}{$array[2]}{direction} eq "incoming") { if(($hash->{helper}{TEMP}{$array[2]}{".last-event"} eq "RING") or (AttrVal($name, "answMachine-is-missed-call", "0") eq "1" and exists($hash->{helper}{TEMP}{$array[2]}{internal_connection}) and $hash->{helper}{TEMP}{$array[2]}{".internal_connection_id"} =~/^4[0-4]$/)) { $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]}{".last-event"} = $array[1]; unless($hash->{helper}{TEMP}{$array[2]}{".deflected"}) { 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 =~ /^\./); } readingsEndUpdate($hash, 1); } else { Log3 $name, 4, "FB_CALLMONITOR ($name) - skipped creating readings/events due to deflection match"; } if($array[1] eq "DISCONNECT") { delete($hash->{helper}{TEMP}{$array[2]}); } } $hash->{PARTIAL} = $buffer; } ##################################### # catches error message during connection setup sub FB_CALLMONITOR_DevIoCallback($$) { my ($hash, $err) = @_; my $name = $hash->{NAME}; if($err) { Log3 $name, 4, "FB_CALLMONITOR ($name) - unable to connect to Fritz!Box: $err"; } } ##################################### # Reconnects to FritzBox in case of disconnects sub FB_CALLMONITOR_Ready($) { my ($hash) = @_; return DevIo_OpenDev($hash, 1, undef, \&FB_CALLMONITOR_DevIoCallback); } ##################################### # 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 =~ /phonebook/) or $attrib eq "reverse-search-phonebook-file" or $attrib eq "fritzbox-remote-phonebook") and $init_done) { $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 "reverse-search-text-file") { return FB_CALLMONITOR_loadTextFile($hash, $value); } if($attrib eq "disable" and $value eq "1") { DevIo_CloseDev($hash); delete($hash->{NEXT_OPEN}); $hash->{STATE} = "disabled"; } elsif($attrib eq "disable" and $value eq "0") { DevIo_OpenDev($hash, 0, undef, \&FB_CALLMONITOR_DevIoCallback); } if($attrib eq "sendKeepAlives" and $value !~ /^none|(?:5|10|15|30)m|1h$/) { return "invalid value $value for $attrib. Allowed values are: none,5m,10m,15m,30m,1h"; } } elsif($cmd eq "del") { if($attrib eq "reverse-search" or $attrib eq "reverse-search-phonebook-file") { delete($hash->{helper}{PHONEBOOK}); } if($attrib eq "reverse-search-cache") { delete($hash->{helper}{CACHE}); } if($attrib eq "reverse-search-text-file") { delete($hash->{helper}{TEXTFILE}); } if($attrib eq "disable") { DevIo_OpenDev($hash, 0, undef, \&FB_CALLMONITOR_DevIoCallback); } } return undef; } ##################################### # receives events, waits for global INITIALIZED or REREADCFG # to initiate the phonebook initialization sub FB_CALLMONITOR_Notify($$) { my ($hash, $device) = @_; my $name = $hash->{NAME}; my $events = deviceEvents($device, undef); return if($device->{NAME} ne "global"); FB_CALLMONITOR_sendKeepAlive($hash) if(grep(m/(?:DELETE)?ATTR $name sendKeepAlives/, @{$events})); return if(!grep(m/^INITIALIZED|REREADCFG$/, @{$events})); FB_CALLMONITOR_readPhonebook($hash); return undef; } ############################################################################################################ # # 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 $status; my $invert_match = undef; my $country_code = AttrVal($name, "country-code", "0049"); my @attr_list = split(/,|\|/, AttrVal($name, "reverse-search", "")); foreach my $method (@attr_list) { # Using internal phonebook if available and enabled if($method eq "phonebook") { if(exists($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}; } elsif(my $result = FB_CALLMONITOR_searchPhonebookWildcards($hash->{helper}{PHONEBOOK}, $number)) { Log3 $name, 4, "FB_CALLMONITOR ($name) - using internal phonebook for reverse search of $number"; return $result; } } if(exists($hash->{helper}{PHONEBOOKS})) { foreach my $pb_id (keys %{$hash->{helper}{PHONEBOOKS}}) { if(defined($hash->{helper}{PHONEBOOKS}{$pb_id}{$number})) { Log3 $name, 4, "FB_CALLMONITOR ($name) - using internal phonebook for reverse search of $number"; return $hash->{helper}{PHONEBOOKS}{$pb_id}{$number}; } elsif(my $result = FB_CALLMONITOR_searchPhonebookWildcards($hash->{helper}{PHONEBOOKS}{$pb_id}, $number)) { Log3 $name, 4, "FB_CALLMONITOR ($name) - using internal phonebook for reverse search of $number"; return $result; } } } } # Using user defined textfile elsif($method eq "textfile") { if(exists($hash->{helper}{TEXTFILE}) and defined($hash->{helper}{TEXTFILE}{$number})) { Log3 $name, 4, "FB_CALLMONITOR ($name) - using textfile for reverse search of $number"; return $hash->{helper}{TEXTFILE}{$number}; } } else { # 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}; } } # Ask dasoertliche.de if($method eq "dasoertliche.de") { unless(($number =~ /^0?[1-9]/ and $country_code eq "0049") or $number =~ /^0049/) { Log3 $name, 4, "FB_CALLMONITOR ($name) - skip using dasoertliche.de for reverse search of $number because of non-german number"; } else { $number =~ s/^0049/0/; # remove country code 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") { $status = "timeout"; undef($result); } } else { #Debug($result); if($result =~ m,(.+?),) { $invert_match = $1; $invert_match = FB_CALLMONITOR_html2txt($invert_match); FB_CALLMONITOR_writeToCache($hash, $number, $invert_match); 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."; } $status = "unknown"; } } } # Ask 11880.com elsif($method eq "11880.com") { unless(($number =~ /^0?[1-9]/ and $country_code eq "0049") or $number =~ /^0049/) { Log3 $name, 4, "FB_CALLMONITOR ($name) - skip using 11880.com for reverse search of $number because of non-german number"; } else { $number =~ s/^0049/0/; # remove country code Log3 $name, 4, "FB_CALLMONITOR ($name) - using 11880.com for reverse search of $number"; $result = GetFileFromURL("https://www.11880.com/suche/".$number."/deutschland", 5, undef, 1); if(not defined($result)) { Log3 $name, 4, "FB_CALLMONITOR ($name) - unable to retrieve result for reverse search of $number via $method"; if(AttrVal($name, "reverse-search-cache", "0") eq "1") { $status = "timeout"; undef($result); } } else { #Debug($result); if($result =~ m,]*class="search-result-list-item"\s+[^>]*data-name="([^"]+)",s or $result =~ m,]*itemprop="name"[^>]*>([^<]+),) { $invert_match = $1; $invert_match = FB_CALLMONITOR_html2txt($invert_match); FB_CALLMONITOR_writeToCache($hash, $number, $invert_match); undef($result); return $invert_match; } elsif(not $result =~ /Leider nichts gefunden/i) { Log3 $name, 3, "FB_CALLMONITOR ($name) - the reverse search result for $number could not be extracted from 11880.com. Please contact the FHEM community."; } $status = "unknown"; } } } # SWITZERLAND ONLY!!! Ask search.ch elsif($method eq "search.ch") { unless(($number =~ /^0?[1-9]/ and $country_code eq "0041") or $number =~ /^0041/) { Log3 $name, 4, "FB_CALLMONITOR ($name) - skip using search.ch for reverse search of $number because of non-swiss number"; } else { my $api_key = AttrVal($name, "apiKeySearchCh", undef); unless(defined($api_key)) { Log3 $name, 1, "FB_CALLMONITOR ($name) - WARNING! no API key for swiss.ch configured. Please obtain an API key from https://tel.search.ch/api/getkey and set attribute apiKeySearchCh with your key"; # use old key Log3 $name, 1, "FB_CALLMONITOR ($name) - using generic API key for reverse search via search.ch. WILL BE REMOVED IN A FUTURE RELEASE"; $api_key = "b0b1207cb7c9d0048867de887aa9a4fd"; } $number =~ s/^0041/0/; # remove country code Log3 $name, 4, "FB_CALLMONITOR ($name) - using search.ch for reverse search of $number"; $result = GetFileFromURL("http://tel.search.ch/api/?key=".urlEncode($api_key)."&maxnum=1&was=".$number, 5, undef, 1); if(not defined($result)) { if(AttrVal($name, "reverse-search-cache", "0") eq "1") { $status = "timeout"; undef($result); } } else { #Log 2, $result; if($result =~ m,(.+?),s) { my $xml = $1; $invert_match = ""; if($xml =~ m,(.+?),) { $invert_match .= $1; } if($xml =~ m,(.+?),) { $invert_match .= " $1"; } if($xml =~ m,(.+?),) { $invert_match .= ", $1"; } $invert_match = FB_CALLMONITOR_html2txt($invert_match); FB_CALLMONITOR_writeToCache($hash, $number, $invert_match); undef($result); return $invert_match; } $status = "unknown"; } } } # Austria ONLY!!! Ask dasschnelle.at elsif($method eq "dasschnelle.at") { unless(($number =~ /^0?[1-9]/ and $country_code eq "0043") or $number =~ /^0043/) { Log3 $name, 4, "FB_CALLMONITOR ($name) - skip using dasschnelle.at for reverse search of $number because of non-austrian number"; } else { $number =~ s/^0043/0/; # remove country code Log3 $name, 4, "FB_CALLMONITOR ($name) - using dasschnelle.at for reverse search of $number"; $result = GetFileFromURL("http://www.dasschnelle.at/ergebnisse?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") { $status = "timeout"; undef($result); } } 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); undef($result); return $invert_match; } elsif(not $result =~ /Ihre Suche nach .* war erfolglos/) { Log3 $name, 3, "FB_CALLMONITOR ($name) - the reverse search result for $number could not be extracted from dasschnelle.at. Please contact the FHEM community."; } $status = "unknown"; } } } # Austria ONLY!!! Ask herold.at elsif($method eq "herold.at") { unless(($number =~ /^0?[1-9]/ and $country_code eq "0043") or $number =~ /^0043/) { Log3 $name, 4, "FB_CALLMONITOR ($name) - skip using herold.at for reverse search of $number because of non-austrian number"; } else { $number =~ s/^0043/0/; # remove country code Log3 $name, 4, "FB_CALLMONITOR ($name) - using herold.at for reverse search of $number"; $result = GetFileFromURL("https://www.herold.at/gelbe-seiten/telefon_".$number."/", 5, undef, 1); if(not defined($result)) { Log3 $name, 4, "FB_CALLMONITOR ($name) - unable to retrieve result for reverse search of $number via $method"; if(AttrVal($name, "reverse-search-cache", "0") eq "1") { $status = "timeout"; undef($result); } } else { #Debug($result); if($result =~ m,data-clickpos="name">([^<]+),) { $invert_match = $1; $invert_match = FB_CALLMONITOR_html2txt($invert_match); FB_CALLMONITOR_writeToCache($hash, $number, $invert_match); undef($result); return $invert_match; } elsif($result !~ /Wir konnten zu den eingegebenen Suchkriterien keine Ergebnisse finden/) { Log3 $name, 3, "FB_CALLMONITOR ($name) - the reverse search result for $number could not be extracted from herold.at. Please contact the FHEM community."; } $status = "unknown"; } } } else { Log3 $name, 3, "FB_CALLMONITOR ($name) - unknown reverse search method $method"; } } } if(AttrVal($name, "reverse-search-cache", "0") eq "1" and defined($status)) { # If no result is available set cache result and return undefined $hash->{helper}{CACHE}{$number} = $status; } return undef; } ##################################### # check a number against wildcard entries in a given phonebook list sub FB_CALLMONITOR_searchPhonebookWildcards($$) { my ($list, $number) = @_; foreach my $key (keys %{$list}) { next if($key !~ /^\+?\d+\*$/); if(defined($list->{$key})) { my $test = $key; $test =~ s/\*$//; if(index($number,$test) == 0) { return $list->{$key}.substr($number,length($test)); } } } 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/&pos;/'/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/(\xdf|ß)/ß/g; $string =~ s/(\xe1|á)/á/g; $string =~ s/(\xe9|é)/é/g; $string =~ s/(\xc1|Á)/Á/g; $string =~ s/(\xc9|É)/É/g; $string =~ s/\\u([a-f\d]{4})/encode('UTF-8',chr(hex($1)))/eig; $string =~ s/<[^>]+>//g; $string =~ s/<//g; $string =~ s/(?:^\s+|\s+$)//g; return $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; if(AttrVal($name, "reverse-search-cache", "0") eq "1") { $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"; } } } } ##################################### # get and reads a FritzBox phonebook sub FB_CALLMONITOR_readPhonebook($;$) { my ($hash, $testPassword) = @_; my $name = $hash->{NAME}; my ($err, $count_contacts, @lines, $phonebook, $pb_hash); delete($hash->{helper}{PHONEBOOK}); delete($hash->{helper}{PHONEBOOKS}); delete($hash->{helper}{IMAGE_URLS}); if(AttrVal($name, "fritzbox-remote-phonebook", "0") eq "1") { if(AttrVal($name, "fritzbox-remote-phonebook-via", "tr064") 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, $pb_hash) = FB_CALLMONITOR_parsePhonebook($hash, $phonebook); $hash->{helper}{PHONEBOOK} = $pb_hash; 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", "tr064") =~ /^(web|tr064)$/) { my $do_with = $1; $err = FB_CALLMONITOR_identifyPhoneBooksViaWeb($hash, $testPassword) if($do_with eq "web"); $err = FB_CALLMONITOR_identifyPhoneBooksViaTR064($hash, $testPassword) if($do_with eq "tr064"); 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}) or exists($excludedIds{$hash->{helper}{PHONEBOOK_NAMES}{$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($do_with eq "web"); ($err, $phonebook) = FB_CALLMONITOR_readRemotePhonebookViaTR064($hash, $phonebookId, $testPassword) if($do_with eq "tr064"); if(defined($err)) { Log3 $name, 2, 'FB_CALLMONITOR ($name) - unable to retrieve phonebook "'.$hash->{helper}{PHONEBOOK_NAMES}{$phonebookId}.'" from FritzBox - '.$err; } else { ($err, $count_contacts, $pb_hash) = FB_CALLMONITOR_parsePhonebook($hash, $phonebook); $hash->{helper}{PHONEBOOKS}{$phonebookId} = $pb_hash; if(defined($err)) { Log3 $name, 2, "FB_CALLMONITOR ($name) - 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}.'"'; } } } delete($hash->{helper}{PHONEBOOK_URL}); FB_CALLMONITOR_downloadImageURLs($hash, $testPassword); } } 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({FileName => $phonebook_file, ForceType => "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, $pb_hash) = 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 { $hash->{helper}{PHONEBOOK} = $pb_hash; 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 $out; if($phonebook =~ /,) { if($phonebook =~ // and $phonebook =~ /]*>(.+?),gcs) { $contact = $1; my $imageURL; if($contact =~ m,([^<>]+),) { $imageURL = $1; $hash->{helper}{IMAGE_URLS}{$imageURL} = [] unless(exists($hash->{helper}{IMAGE_URLS}{$imageURL})); } if($contact =~ m,(.+?),) { $contact_name = $1; while($contact =~ m,]*?type="([^<>"]+?)"[^<>]*?>([^<>"]+?),gs) { if($1 ne "intern" and $1 ne "memo") { $number = FB_CALLMONITOR_normalizePhoneNumber($hash, $2); $count_contacts++; Log3 $name, 4, "FB_CALLMONITOR ($name) - found $contact_name with number $number"; $out->{$number} = FB_CALLMONITOR_html2txt($contact_name); if($imageURL) { Log3 $name, 5, "FB_CALLMONITOR ($name) - found image for $contact_name with number $number"; push @{$hash->{helper}{IMAGE_URLS}{$imageURL}}, $number; } undef $number; } } undef $contact_name; } } } return (undef, $count_contacts, $out); } 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)); if($file ne "" and -r $file) { delete($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*$/) { chomp $line; @tmpline = split("\\|", $line, 2); 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 the reverse search cache from file sub FB_CALLMONITOR_loadTextFile($;$) { my ($hash, $file) = @_; my @file; my @tmpline; my $count_contacts; my $name = $hash->{NAME}; my $err; $file = AttrVal($hash->{NAME}, "reverse-search-text-file", "") unless(defined($file)); if($file ne "" and -r $file) { delete($hash->{helper}{TEXTFILE}); Log3 $hash->{NAME}, 3, "FB_CALLMONITOR ($name) - loading textfile $file"; ($err, @file) = FileRead({FileName => $file, ForceType => "file"}); unless(defined($err) and $err) { foreach my $line (@file) { $line =~ s/(,.*?)#.*$/$1/g; $line =~ s,//.*$,,g; if((not $line =~ /^\s*$/) and $line =~ /,/) { chomp $line; @tmpline = split(/,/, $line,2); if(@tmpline == 2) { $hash->{helper}{TEXTFILE}{FB_CALLMONITOR_normalizePhoneNumber($hash, $tmpline[0])} = trim($tmpline[1]); } } } $count_contacts = scalar keys %{$hash->{helper}{TEXTFILE}}; Log3 $name, 2, "FB_CALLMONITOR ($name) - read ".($count_contacts > 0 ? $count_contacts : "no")." contact".($count_contacts == 1 ? "" : "s")." from textfile"; } else { Log3 $name, 3, "FB_CALLMONITOR ($name) - could not open textfile: $err"; } } else { @tmpline = ("###########################################################################################", "# This file was created by FHEM and contains user defined reverse search entries #", "# Please insert your number entries in the following format: #", "# #", "# , #", "# , #", "# #", "###########################################################################################", "# e.g.", "# 0123/456789,Mum", "# +49 123 45 67 8,Dad", "# 45678,Boss", "####" ); $err = FileWrite({FileName => $file, ForceType => "file"},@tmpline); Log3 $name, 3, "FB_CALLMONITOR ($name) - unable to create textfile $file: $err" if(defined($err) and $err ne ""); } } ##################################### # 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. Maybe 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}); return "no password available to access FritzBox. Please set your FRITZ!Box password via 'set ".$hash->{NAME}." password '" unless(defined($fb_pw)); my $telnet = Net::Telnet->new(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}); return (undef, join('', @FBPhoneBook)); } ##################################### # performs a HTTP based request via TR-064 port with authentication sub FB_CALLMONITOR_requestHTTPviaTR064($$$$;$$) { my ($hash, $url, $data, $header, $auth, $testPassword) = @_; my $name = $hash->{NAME}; my ($fb_ip,undef) = split(/:/, ($hash->{DeviceName}), 2); my ($fb_user, $fb_pw); my $param; my ($err, $response); if($auth) { $fb_user = AttrVal($name, "fritzbox-user", "admin"); $hash->{helper}{READ_PWD} = 1; $fb_pw = FB_CALLMONITOR_readPassword($hash, $testPassword); delete($hash->{helper}{READ_PWD}); unless(defined($fb_pw)) { $hash->{helper}{PWD_NEEDED} = 1; return "no password available to access FritzBox. Please set your FRITZ!Box password via 'set ".$hash->{NAME}." password '"; } } my $tr064_base_url = "http://".($auth ? urlEncode($fb_user).":".urlEncode($fb_pw)."\@" : "") ."$fb_ip:49000"; $param->{noshutdown} = 1; $param->{timeout} = AttrVal($name, "fritzbox-remote-timeout", 5); $param->{loglevel} = 4; $param->{digest} = 1; $param->{hideurl} = 1; $param->{sslargs} = { SSL_verify_mode => 0, SSL_cipher_list => 'DEFAULT:!DH' }; # workaround for newer OpenSSL-Libraries who do not allow weak DH based ciphers (Forum: #80281) unless($hash->{helper}{TR064}{SECURITY_PORT}) { my $get_security_port = ''. ''. ''. ''. ''. ''; $param->{url} = "$tr064_base_url/upnp/control/deviceinfo"; $param->{header} = "SOAPACTION: urn:dslforum-org:service:DeviceInfo:1#GetSecurityPort\r\nContent-Type: text/xml; charset=utf-8"; $param->{data} = $get_security_port; Log3 $name, 4, "FB_CALLMONITOR ($name) - request SSL port for TR-064 access via method GetSecurityPort:\n$get_security_port"; my ($err, $response) = HttpUtils_BlockingGet($param); if($err ne "") { Log3 $name, 3, "FB_CALLMONITOR ($name) - error while requesting security port: $err"; return "error while requesting phonebooks: $err"; } if($response eq "" and exists($param->{code})) { Log3 $name, 3, "FB_CALLMONITOR ($name) - received http code ".$param->{code}." without any data after requesting security port via TR-064"; return "received no data after requesting security port via TR-064"; } Log3 $name, 5, "FB_CALLMONITOR ($name) - received TR-064 method GetSecurityPort response:\n$response"; if($response =~ /(\d+)<\/NewSecurityPort>/) { $tr064_base_url = "https://".($auth ? urlEncode($fb_user).":".urlEncode($fb_pw)."\@" : "")."$fb_ip:$1"; $hash->{helper}{TR064}{SECURITY_PORT} = $1; } } else { $tr064_base_url = "https://".($auth ? urlEncode($fb_user).":".urlEncode($fb_pw)."\@" : "")."$fb_ip:".$hash->{helper}{TR064}{SECURITY_PORT}; } $param->{url} = "$tr064_base_url$url"; $param->{header} = $header if($header); if($data) { $param->{data} = $data if($data); Log3 $name, 4, "FB_CALLMONITOR ($name) - sending TR-064 request:\n$data"; } else { Log3 $name, 4, "FB_CALLMONITOR ($name) - requesting TR-064 URL: $url"; } ($err, $response) = HttpUtils_BlockingGet($param); if($err ne "") { if(exists($param->{code}) and $param->{code} eq "401") { $hash->{helper}{PWD_NEEDED} = 1; Log3 $name, 3, "FB_CALLMONITOR ($name) - unable to login via TR-064, wrong user/password"; return "unable to login via TR-064, wrong user/password"; } else { Log3 $name, 3, "FB_CALLMONITOR ($name) - error while requesting TR-064 URL $url: $err"; return "error while requesting TR-064 URL $url: $err"; } } if($response eq "" and exists($param->{code})) { Log3 $name, 3, "FB_CALLMONITOR ($name) - received http code ".$param->{code}." without any data after requesting TR-064 URL $url"; return "received no data after requesting TR-064 URL $url"; } if($param->{httpheader} =~ m,^Content-Type:\s*text,mi) { Log3 $name, 5, "FB_CALLMONITOR ($name) - received TR-064 response for URL $url:\n$response"; } else { Log3 $name, 5, "FB_CALLMONITOR ($name) - received TR-064 response for URL $url"; } return (undef, $response, $param); } ##################################### # execute TR-064 methods via HTTP/SOAP request sub FB_CALLMONITOR_requestTR064($$$$;$$) { my ($hash, $path, $command, $type, $command_arg, $testPassword) = @_; my $name = $hash->{NAME}; # éxecute the TR-064 request my $soap_request = ''. ''. ''. "".($command_arg ? $command_arg : "")."". ''. ''; my $header = "SOAPACTION: $type#$command\r\nContent-Type: text/xml; charset=utf-8"; return FB_CALLMONITOR_requestHTTPviaTR064($hash, $path, $soap_request, $header, 1, $testPassword); } ##################################### # identifys the phonebooks defined on the FritzBox via TR064 interface (SOAP) and generate download url sub FB_CALLMONITOR_identifyPhoneBooksViaTR064($;$) { my ($hash, $testPassword) = @_; my $name = $hash->{NAME}; my ($err, $data) = FB_CALLMONITOR_requestTR064($hash, "/upnp/control/x_contact", "GetPhonebookList", "urn:dslforum-org:service:X_AVM-DE_OnTel:1", undef, $testPassword); return "unable to identify phonebooks via TR-064: $err" if($err); my @phonebooks; # read list response (TR-064 id's: "0,1,2,...") if($data =~ m,(.+?),si) { @phonebooks = split(",",$1); Log3 $name, 3, "FB_CALLMONITOR ($name) - found ".scalar @phonebooks." phonebooks"; } else { Log3 $name, 3, "FB_CALLMONITOR ($name) - no phonebooks found"; return "no phonebooks could be found"; } delete($hash->{helper}{PHONEBOOK_NAMES}); delete($hash->{helper}{PHONEBOOK_URL}); # request name and FritzBox phone id for each list item foreach (@phonebooks) { my $item_id = $_; my $phb_id; Log3 $name, 5, "FB_CALLMONITOR ($name) - requesting phonebook description for id $item_id"; ($err, $data) = FB_CALLMONITOR_requestTR064($hash, "/upnp/control/x_contact", "GetPhonebook", "urn:dslforum-org:service:X_AVM-DE_OnTel:1", "$item_id", $testPassword); if ($err) { Log3 $name, 3, "FB_CALLMONITOR ($name) - error while requesting phonebook description for id $item_id: $err"; return "error while requesting phonebook description for id $item_id: $err"; } Log3 $name, 5, "FB_CALLMONITOR ($name) - received response with phonebook description for id $item_id:\n$data"; if($data =~ m,(.+?).*?.*?pbid=(\d+)\D*?,si) { $phb_id = $2; $hash->{helper}{PHONEBOOK_NAMES}{$phb_id} = $1; Log3 $name, 4, "FB_CALLMONITOR ($name) - found phonebook: $1 - $2"; } if($data =~ m,(.*?),i) { $hash->{helper}{PHONEBOOK_URL}{$phb_id} = $1; $hash->{helper}{PHONEBOOK_URL}{$phb_id} =~ s/&/&/g; Log3 $name, 4, "FB_CALLMONITOR ($name) - found phonebook url for id $phb_id: ".$hash->{helper}{PHONEBOOK_URL}{$phb_id}; } } Log3 $name, 4, "FB_CALLMONITOR ($name) - phonebooks found: ".join(", ", map { $hash->{helper}{PHONEBOOK_NAMES}{$_}." (id: $_)" } sort keys %{$hash->{helper}{PHONEBOOK_NAMES}}) if(exists($hash->{helper}{PHONEBOOK_NAMES})); # get deflections delete($hash->{helper}{DEFLECTIONS}); Log3 $name, 5, "FB_CALLMONITOR ($name) - requesting deflection list"; ($err, $data) = FB_CALLMONITOR_requestTR064($hash, "/upnp/control/x_contact", "GetDeflections", "urn:dslforum-org:service:X_AVM-DE_OnTel:1",undef, $testPassword); if ($err) { Log3 $name, 3, "FB_CALLMONITOR ($name) - error while requesting deflection list: $err"; return "error while requesting deflection list: $err"; } $data =~ s/<//g; # extract deflection list while($data =~ /(.*?)<\/Item>/gcs) { my $deflection_item = $1; my %values; while($deflection_item =~ m,<(?:\w+:)?(\w+)>([^<]+),gcs) { $values{$1} = $2; } $hash->{helper}{DEFLECTIONS}{$values{DeflectionId}} = \%values; } Log3 $name, 3, "FB_CALLMONITOR ($name) - found ".(scalar keys %{$hash->{helper}{DEFLECTIONS}})." blocking rules (deflections)" if(exists($hash->{helper}{DEFLECTIONS})); delete($hash->{helper}{PWD_NEEDED}); return undef; } ##################################### # loads internal and online phonebooks from extern FritzBox via web interface (http) sub FB_CALLMONITOR_readRemotePhonebookViaTR064($$;$) { my ($hash, $phonebookId, $testPassword) = @_; my $name = $hash->{NAME}; my ($fb_ip,undef) = split(/:/, ($hash->{DeviceName}), 2); return "unknown phonebook id: $phonebookId" unless(exists($hash->{helper}{PHONEBOOK_NAMES}{$phonebookId})); return "unknown phonebook url:" unless(exists($hash->{helper}{PHONEBOOK_URL}{$phonebookId})); my $phb_url = $hash->{helper}{PHONEBOOK_URL}{$phonebookId}; my $param; $param->{url} = $phb_url; $param->{noshutdown} = 1; $param->{timeout} = AttrVal($name, "fritzbox-remote-timeout", 5); $param->{loglevel} = 4; 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"; } return (undef, $phonebook); } ##################################### # retrieves a session id to download files via TR-064 (phonebook contact images) sub FB_CALLMONITOR_getSIDviaTR064($) { my ($hash, $testPassword) = @_; my $name = $hash->{NAME}; my ($err, $response) = FB_CALLMONITOR_requestTR064($hash, "/upnp/control/deviceconfig", "X_AVM-DE_CreateUrlSID", "urn:dslforum-org:service:DeviceConfig:1", undef, $testPassword); if($err) { Log3 $name, 3, "FB_CALLMONITOR ($name) - error while requesting session id via TR064: $err"; return undef; } if($response =~ m,(.+?),) { return $1; } return undef; } ##################################### # downloads all available image URL's via TR-064 sub FB_CALLMONITOR_downloadImageURLs($$) { my ($hash, $testPassword) = @_; my $name = $hash->{NAME}; if($hash->{helper}{IMAGE_URLS} and AttrVal($name,"contactImageViaTR064", "1") eq "1") { my $sid = FB_CALLMONITOR_getSIDviaTR064($hash); my $local_path = AttrVal($name,"contactImageDirectory", undef); return unless(defined($sid) and defined($local_path)); if(! -d $local_path) { eval { use File::Path }; if($@) { Log3 $name, 3, "FB_CALLMONITOR ($name) - unable to load File::Path perl module to create contact images directory '$local_path'. Please create it by yourself"; return undef; } eval {File::Path::make_path($local_path) }; if($@) { Log3 $name, 3, "FB_CALLMONITOR ($name) - unable to create contact images directory '$local_path': $@"; return undef; } } foreach my $url (keys %{$hash->{helper}{IMAGE_URLS}}) { my ($err, $file, $param) = FB_CALLMONITOR_requestHTTPviaTR064($hash, $url."&".$sid, undef, undef, 0, $testPassword); if($err) { Log3 $name, 3, "FB_CALLMONITOR ($name) - error while requesting image URL $url via TR064: $err"; next; } if($param->{httpheader} =~ /Content-Disposition: attachment; filename="[^"\s]+(\.\w+)"$/m) { my $suffix = $1; foreach my $number (@{$hash->{helper}{IMAGE_URLS}{$url}}) { my $filename = $local_path."/".$number.$suffix; my $result = FileWrite({FileName => $local_path."/".$number.$suffix, ForceType=> "file", NoNL=> 1}, $file); if($result) { Log3 $name, 3, "FB_CALLMONITOR ($name) - error while writing image $filename: $result"; } } } } Log3 $name, 2, "FB_CALLMONITOR ($name) - downloaded ".(scalar keys %{$hash->{helper}{IMAGE_URLS}})." contact images from all phonebooks"; } } ##################################### # returns the filename of a corresponding contact image if exist. sub FB_CALLMONITOR_getContactImage($$) { my ($hash, $number) = @_; my $name = $hash->{NAME}; my $local_path = AttrVal($name,"contactImageDirectory", undef); return undef unless(defined($local_path)); $local_path =~ s,/+$,,; opendir(DIR, $local_path) or return undef; while (my $file = readdir(DIR)) { next if($file =~ /^\./); next if(-d "$local_path/$file"); next unless($file =~ /^$number\./); return $file; } return AttrVal($name,"contactDefaultImage", "none"); } ##################################### # 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}); return "no password available to access FritzBox. Please set your FRITZ!Box password via 'set ".$hash->{NAME}." password '" 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"[^>]*>(.+?),s) { $data = $1; } delete($hash->{helper}{PHONEBOOK_NAMES}); while($data =~ m,]*for="uiBookid:(\d+)"[^>]*>\s*(.+?)\s*,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}); 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}); return "no password available to access FritzBox. Please set your FRITZ!Box password via 'set ".$hash->{NAME}." password '" 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}); 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 (undef, $enc_pwd) = FB_CALLMONITOR_encrypt($password, $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)) { my (undef, $dec_pwd) = FB_CALLMONITOR_decrypt($password, $key); return $dec_pwd if($hash->{helper}{READ_PWD}); } else { $hash->{helper}{PWD_NEEDED} = 1; return undef; } } ##################################### # normalizes a formated phone number sub FB_CALLMONITOR_normalizePhoneNumber($$) { my ($hash, $number) = @_; my $name = $hash->{NAME}; my $area_code = AttrVal($name, "local-area-code", ""); my $country_code = AttrVal($name, "country-code", "0049"); $number =~ s/\s//g; # Remove spaces $number =~ s/^(\#[0-9]{1,10}\#)//g; # Remove phone control codes $number =~ s/^\+/00/g; # Convert leading + to 00 country extension $number =~ s/[^\d\*#]//g if(not $number =~ /@/); # Remove anything else isn't a number if it is no VoIP number $number =~ s/^$country_code/0/g; # Replace own country code with leading 0 if($number =~ /^\d/ and $number !~ /^0/ and $number !~ /^11/ and $number !~ /@/ and $area_code =~ /^0[1-9]\d+$/) { $number = $area_code.$number; } return $number; } ##################################### # decrypt an encrypted password sub FB_CALLMONITOR_decrypt($$) { my ($password, $key) = @_; return undef unless(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 (undef, $dec_pwd); } ##################################### # encrypts a password sub FB_CALLMONITOR_encrypt($$) { my ($password, $key) = @_; return undef unless(defined($password)); if(eval "use Digest::MD5;1") { $key = Digest::MD5::md5_hex(unpack "H*", $key); $key .= Digest::MD5::md5_hex($key); } my $enc_pwd = ''; for my $char (split //, $password) { my $encode=chop($key); $enc_pwd.=sprintf("%.2x",ord($char)^ord($encode)); $key=$encode.$key; } return (undef, $enc_pwd); } sub FB_CALLMONITOR_checkNumberForDeflection($$) { my ($hash, $number) = @_; my $name = $hash->{NAME}; my $ret = 0; if(exists($hash->{helper}{DEFLECTIONS}) and AttrVal($name,"check-deflections",0)) { my $deflection_count = scalar keys %{$hash->{helper}{DEFLECTIONS}}; Log3 $name, 4, "FB_CALLMONITOR ($name) - check ".(defined($number) ? $number : "unknown number")." against deflection rules (".$deflection_count." rule".($deflection_count ==1 ? "" : "s").")"; foreach my $item (values %{$hash->{helper}{DEFLECTIONS}}) { next unless($item->{Enable}); # next if rule not enabled next if(!$item->{Type}); if($item->{Type} eq "fromNumber" and $item->{Number} and $number) { my $tmp = $item->{Number}; $ret = 1 if($number =~ /^0?$tmp/); } elsif($item->{Type} eq "fromPB" and $item->{PhonebookID} and $number) { $ret = 1 if(exists($hash->{helper}{PHONEBOOKS}) and exists($hash->{helper}{PHONEBOOKS}{$item->{PhonebookID}}) and exists($hash->{helper}{PHONEBOOKS}{$item->{PhonebookID}}{$number})); } elsif($item->{Type} eq "fromAnonymous") { $ret = 1 unless(defined($number)); } } } Log3 $name, 4, "FB_CALLMONITOR ($name) - found matching deflection. call will be ignored" if($ret); return $ret; } sub FB_CALLMONITOR_sendKeepAlive($) { my ($hash) = @_; my $name = $hash->{NAME}; my $sendKeepAlives = AttrVal($name, "sendKeepAlives", "none"); my $values = { "5m" => 300, "10m" => 600, "15m" => 900, "30m" => 1800, "1h" => 3600 }; if($sendKeepAlives ne "none" and DevIo_IsOpen($hash)) { Log3 $name, 5, "FB_CALLMONITOR ($name) - sending keep-alive"; DevIo_SimpleWrite($hash, "\n", 2); if($values->{$sendKeepAlives}) { InternalTimer(gettimeofday()+$values->{$sendKeepAlives}, "FB_CALLMONITOR_sendKeepAlive", $hash); } else { Log3 $name, 3, "FB_CALLMONITOR ($name) - unsupported value for attribute sendKeepAlives: $sendKeepAlives. discard sending keep-alive"; } } else { RemoveInternalTimer($hash, "FB_CALLMONITOR_sendKeepAlive"); } return undef; } 1; =pod =item helper =item summary provides realtime telephone events of a AVM FRITZ!Box via LAN connection =item summary_DE stellt Telefonereignisse einer AVM FRITZ!Box via LAN zur Verfügung =begin html

FB_CALLMONITOR

=end html =begin html_DE

FB_CALLMONITOR

=end html_DE =cut