# $Id: 72_FB_CALLLIST.pm 25271 2021-11-28 09:40:15Z markusbloch $
##############################################################################
#
#     72_FB_CALLLIST.pm
#     Creates a call list based on the events generated by a FB_CALLMONITOR instance
#
#     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 <http://www.gnu.org/licenses/>.
#
##############################################################################

package main;

use strict;
use warnings;

use POSIX;
use MIME::Base64;
use Data::Dumper;
use HttpUtils;

sub
FB_CALLLIST_Initialize($)
{
    my ($hash) = @_;

    $hash->{SetFn}     = "FB_CALLLIST_Set";
    $hash->{DefFn}     = "FB_CALLLIST_Define";
    $hash->{NotifyFn}  = "FB_CALLLIST_Notify";
    $hash->{RenameFn}  = "FB_CALLLIST_Rename";
    $hash->{DeleteFn}  = "FB_CALLLIST_Delete";
    $hash->{AttrFn}    = "FB_CALLLIST_Attr";
    $hash->{UndefFn}   = "FB_CALLLIST_Undef";
    $hash->{AttrList}  =  "number-of-calls:1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40 ".
                          "internal-number-filter:textField-long ".
                          "icon-mapping:textField-long ".
                          "connection-mapping:textField-long ".
                          "external-mapping:textField-long ".
                          "create-readings:0,1 ".
                          "visible-columns:sortable-strict,row,state,timestamp,image,name,number,internal,external,connection,duration ".
                          "show-icons:1,0 ".
                          "list-type:all,incoming,outgoing,missed-calls,completed,active " .
                          "time-format-string ".
                          "list-order:ascending,descending ".
                          "answMachine-is-missed-call:0,1 ".
                          "language:de,en ".
                          "disable:0,1,2,3 ".
                          "processEventsWhileDisabled:0,1 ".
                          "number-cmd ".
                          "disabledForIntervals ".
                          "do_not_notify:0,1 ".
                          "expire-calls-after ".
                          "no-heading:0,1 ".
                          "no-table-header:0,1 ".
                          "contactImageDirectory ".
                          "contactDefaultImage ".
                          $readingFnAttributes;

    $hash->{FW_detailFn}  = "FB_CALLLIST_makeTable";
    $hash->{FW_summaryFn} = "FB_CALLLIST_makeTable";
    $hash->{FW_atPageEnd} = 1;
}

#####################################
# Define function
sub FB_CALLLIST_Define($$)
{
    my ($hash, $def) = @_;
    my @a = split("[ \t][ \t]*", $def);
    my $retval = undef;
    my $name = $a[0];
    my $callmonitor = $a[2];

    return "wrong define syntax: you must specify a device name for using FB_CALLLIST" if(!defined($callmonitor));
    return "wrong define syntax: define <name> FB_CALLLIST <name>" if(@a != 3);

    if($init_done)
    {
        return "define error: the selected device $callmonitor does not exist." unless(defined($defs{$callmonitor}));

        Log3 $name, 3, "FB_CALLLIST ($name) - WARNING - selected device $callmonitor ist not of type FB_CALLMONITOR" unless($defs{$callmonitor}->{TYPE} eq "FB_CALLMONITOR");
    }

    $hash->{FB} = $callmonitor;
    $hash->{NOTIFYDEV} = "global,".$callmonitor;
    $hash->{STATE} = 'Initialized';
    $hash->{helper}{DEFAULT_COLUMN_ORDER} = "row,state,timestamp,name,number,internal,external,connection,duration";

    FB_CALLLIST_loadList($hash);

    return undef;
}

#####################################
# AttrFn for importing filter expressions and cleanup list when user set an attribute
sub FB_CALLLIST_Attr($@)
{
    my ($cmd, $name, $attrib, $value) = @_;
    my $hash = $defs{$name};

    if($cmd eq "set")
    {
        if($attrib eq "internal-number-filter")
        {
            if( $value =~ m/^{.*}$/s )
            {
                my $table = eval $value;

                if($table and (ref($table) eq 'HASH'))
                {
                    $hash->{helper}{INTERNAL_FILTER} = $table;
                    Log3 $name, 4, "FB_CALLLIST ($name) - filter stored as hash: $value";
                }
                else
                {
                    return "(line-filter) is not a valid hash: $value";
                }
            }
            else
            {
                delete($hash->{helper}{INTERNAL_FILTER});

                foreach my $item (split("[ \t,][ \t,]*",$value))
                {
                    $hash->{helper}{INTERNAL_FILTER}{$item} = $item;
                }

                Log3 $name, 4, "FB_CALLLIST ($name) - filter stored as list $value";
            }
        }
        elsif($attrib eq "connection-mapping")
        {
            if($value and $value =~ m/^{.*}$/s )
            {
                my $table = eval $value;

                return "invalid connection mapping table: $@" if($@);
                return "connection mapping table is not a hash structure: $value" if($table and ref($table) ne 'HASH');
            }
            else
            {
                return "invalid connection mapping table: $value";
            }
        }
        elsif($attrib eq "icon-mapping")
        {
            if($value and $value =~ m/^{.*}$/s )
            {
                my $table = eval $value;

                return "invalid icon mapping table: $@" if($@);
                return "icon mapping table is not a hash structure: $value" if($table and ref($table) ne 'HASH');
            }
            else
            {
                return "invalid icon mapping table: $value";
            }
        }
        elsif($attrib eq "external-mapping")
        {
            if($value and $value =~ m/^{.*}$/s )
            {
                my $table = eval $value;

                return "invalid external mapping table: $@" if($@);
                return "external mapping table is not a hash structure: $value" if($table and ref($table) ne 'HASH');
            }
            else
            {
                return "invalid external mapping table: $value";
            }
        }
        elsif($attrib eq "expire-calls-after")
        {
            if($value !~ /^\s*\d+(?:\.\d+)?(?:\s+(?:minute|hour|day|week|month|year)s?)?\s*$/i)
            {
                return "not a valid time frame value. See commandref for the correct syntax.";
            }
        }
        elsif($attrib eq "number-of-calls")
        {
            if($value !~ /^\d+$/ or $value <= 0)
            {
                return "not a valid positive integer value";
            }
        }
    }
    elsif($cmd eq "del")
    {
        if($attrib eq "internal-number-filter")
        {
            delete($hash->{helper}{INTERNAL_FILTER});
        }
        elsif($attrib eq "connection-mapping")
        {
            delete($hash->{helper}{CONNECTION_MAP});
        }
        elsif($attrib eq "icon-mapping")
        {
            delete($hash->{helper}{ICON_MAP});
        }
        elsif($attrib eq "external-mapping")
        {
            delete($hash->{helper}{EXTERNAL_MAP});
        }
    }
}

#####################################
# SetFn for clearing the list
sub FB_CALLLIST_Set($@)
{
    my ($hash, $name, $cmd, $value) = @_;

    my @list = FB_CALLLIST_getAllItemLines($hash);
    
    my $index_list = join(",", (1..@list));
    my $usage = "Unknown argument $cmd, choose one of clear:noArg removeItem:$index_list";

    
    if($cmd eq "clear")
    {
        delete($hash->{helper}{DATA});

        # update readings if neccessary
        FB_CALLLIST_createReadings($hash);
        
        # Inform all FHEMWEB clients
        FB_CALLLIST_updateFhemWebClients($hash);

        # Delete stored list
        FB_CALLLIST_saveList($hash);
    }
    elsif($cmd eq "removeItem" and defined($value) and $value =~ /^\d+$/)
    {
        my $item = $list[$value - 1];
        
        return $usage unless(defined($item));
        
        FB_CALLLIST_deleteItem($hash, $item->{index});
        
        FB_CALLLIST_createReadings($hash);
        
        FB_CALLLIST_saveList($hash);
    }
    else
    {
        return $usage;
    }
}

#####################################
# If Device is deleted, delete stored list data
sub FB_CALLLIST_Delete($)
{
    my ($hash, $name) = @_;

    my $err = setKeyValue("FB_CALLLIST-$name", undef);

    Log3 $name, 3, "FB_CALLLIST ($name) - error while deleting the current call list: $err" if(defined($err));
}

#####################################
# If Device is rename, copy the current data
sub FB_CALLLIST_Rename($$)
{
    my ($new,$old) = @_;

    my (undef, $data) = getKeyValue("FB_CALLLIST-".$old);

    return undef unless(defined($data));

    setKeyValue("FB_CALLLIST-".$new, $data);
    setKeyValue("FB_CALLLIST-".$old, undef);

    return undef;
}

#####################################
# If device is deleted or rereadcfg is executed
sub FB_CALLLIST_Undef($$)
{
    my ($hash, $name) = @_;

    RemoveInternalTimer($name, "FB_CALLLIST_deleteExpiredCalls");
}

#####################################
# NotifyFn is trigger upon changes on FB_CALLMONITOR device. Imports the call data into call list
sub FB_CALLLIST_Notify($$)
{
    my ($hash,$dev) = @_;

    return undef if(!defined($hash) or !defined($dev));

    my $name = $hash->{NAME};
    my $events = deviceEvents($dev,0);

    if($dev->{NAME} eq "global")
    {
        my $callmonitor = $hash->{FB};

        if(grep(m/^(?:ATTR $name external-mapping .*|INITIALIZED|REREADCFG)$/, @{$events}))
        {
            my $value = AttrVal($name,"external-mapping","");
            my $table = eval($value);

            if($table and ref($table) eq 'HASH')
            {
                $hash->{helper}{EXTERNAL_MAP} = $table;
                Log3 $name, 4, "FB_CALLLIST ($name) - external map stored as hash: $value";
            }
        }

        if(grep(m/^(?:ATTR $name connection-mapping .*|INITIALIZED|REREADCFG)$/, @{$events}))
        {
            my $value = AttrVal($name,"connection-mapping","");
            my $table = eval($value);

            if($table and ref($table) eq 'HASH')
            {
                $hash->{helper}{CONNECTION_MAP} = $table;
                Log3 $name, 4, "FB_CALLLIST ($name) - connection map stored as hash: $value";
            }
        }

        if(grep(m/^(?:ATTR $name icon-mapping .*|INITIALIZED|REREADCFG)$/, @{$events}))
        {
            my $value = AttrVal($name,"icon-mapping","");

            $value =~ s/"([^"]+?)"/'$1'/g; # workaround for array variable interpretation

            my $table = eval($value);

            if($table and ref($table) eq 'HASH')
            {
                $hash->{helper}{ICON_MAP} = $table;
                Log3 $name, 4, "FB_CALLLIST ($name) - icon map stored as hash: $value";
            }
        }

        if(grep(m/^(?:INITIALIZED|REREADCFG)$/, @{$events}))
        {
            Log3 $name, 2, "FB_CALLLIST ($name) - WARNING - the selected device $callmonitor does not exist" unless(IsDevice($callmonitor));
            Log3 $name, 2, "FB_CALLLIST ($name) - WARNING - selected device $callmonitor ist not of type FB_CALLMONITOR" if(IsDevice($callmonitor) and !IsDevice($callmonitor,"FB_CALLMONITOR"));
        }

        if(grep(m/^(?:ATTR $name .*|DELETEATTR $name.*|INITIALIZED|REREADCFG)$/, @{$events}))
        {
            # delete all outdated calls according to attribute list-type, internal-number-filter and number-of-calls
            FB_CALLLIST_cleanupList($hash);

            if(grep(m/^(?:ATTR|DELETEATTR)/, @{$events}))
            {
                # Inform all FHEMWEB clients
                FB_CALLLIST_updateFhemWebClients($hash);

                # Update readings
                FB_CALLLIST_createReadings($hash);
            }

            # save current list state to file/configDB
            FB_CALLLIST_saveList($hash);
        }

        # detect renaming of attached FB_CALLMONITOR
        if(defined($callmonitor) and grep(/^RENAMED $callmonitor \S+$/, @{$events}))
        {
            my ($new) = map((/^RENAMED $callmonitor (\S+)$/ ? $1 : () ), @{$events});

            if(defined($new))
            {
                Log3 $name, 3, "FB_CALLLIST ($name) - configured callmonitor definition $callmonitor was renamed to $new";
                $hash->{DEF} = $new;
                $hash->{NOTIFYDEV} = "global,".$new;
                $hash->{FB} = $new;
            }
        }

        return undef;
    }

    my $fb = $dev->{NAME};

    return undef if(IsDisabled($name) and AttrVal($name, "processEventsWhileDisabled", "0") eq "0");
    return undef if($fb ne $hash->{FB});
    return undef if(!grep(m/^event:/,@{$events}));

    my $event = ReadingsVal($fb, "event", undef);
    my $call_id = ReadingsVal($fb, "call_id", undef);

    Log3 $name, 4, "FB_CALLLIST ($name) - start processing event $event for Call-ID $call_id";

    if(exists($hash->{helper}{LAST_EVENT}) and exists($hash->{helper}{LAST_CALL_ID}) and $event eq $hash->{helper}{LAST_EVENT} and $call_id eq $hash->{helper}{LAST_CALL_ID})
    {
        Log3 $name, 4, "FB_CALLLIST ($name) - already processed event $event for Call-ID $call_id, skipping...";
        return undef
    }
    else
    {
        $hash->{helper}{LAST_EVENT} = $event;
        $hash->{helper}{LAST_CALL_ID} = $call_id;
    }

    if(exists($hash->{helper}{INTERNAL_FILTER}))
    {
        Log3 $name, 5, "FB_CALLLIST ($name) - internal-number-filter is defined, checking if internal number is allowed";

        my $line_number = ReadingsVal($fb, "internal_number", undef);

        if(defined($line_number) and not exists($hash->{helper}{INTERNAL_FILTER}{$line_number}))
        {
            Log3 $name, 5, "FB_CALLLIST ($name) - internal number $line_number does not match the current internal-number-filter. skipping call.";
            return undef;
        }

        Log3 $name, 5, "FB_CALLLIST ($name) - call passed the internal-number-filter. proceeding...";
    }

    my $data;

    if($event =~ /^call|ring$/)
    {
        # end running calls with same call-id (Forum: #62468)
        while(my $old_data = FB_CALLLIST_getDataReference($hash, $call_id))
        {
            Log3 $name, 4, "FB_CALLLIST ($name) - found running call with same call id: $old_data\n".Dumper($old_data);

            delete($old_data->{running_call});
            $old_data->{finished} = gettimeofday();
            $old_data->{call_duration} = 0;
        }

        my $timestamp = gettimeofday();

        $hash->{helper}{DATA}{$timestamp} = undef;

        $data = \%{$hash->{helper}{DATA}{$timestamp}};
        $data->{internal_index} = $timestamp;
        $data->{external_number} = ReadingsVal($fb, "external_number", undef);
        $data->{external_name} = ReadingsVal($fb, "external_name", undef);
        $data->{external_connection} = ReadingsVal($fb, "external_connection", undef);
        $data->{internal_number} =  ReadingsVal($fb, "internal_number", undef);
        $data->{direction} = ReadingsVal($fb, "direction", undef);
        $data->{running_call} = 1;
        $data->{call_id} = $call_id;

        if($data->{direction} eq "outgoing")
        {
            $data->{internal_connection} = ReadingsVal($fb, "internal_connection", undef);
        }

        Log3 $name, 5, "FB_CALLLIST ($name) - created new data hashcreated new data hash: $data";
    }
    else
    {
        $data = FB_CALLLIST_getDataReference($hash, $call_id);
        Log3 $name, 5, "FB_CALLLIST ($name) - found old data hash: $data" if($data);
    }

    if(!$data)
    {
        Log3 $name, 4, "FB_CALLLIST ($name) - no data for this call in list. seams to be filtered out. skipping further processing...";
        return undef;
    }

    if($event eq "connect")
    {
        $data->{internal_connection} = ReadingsVal($fb, "internal_connection", undef)  if($data->{direction} eq "incoming");

        Log3 $name, 5, "FB_CALLLIST ($name) - processed connect event for call id $call_id";
    }

    if($event eq "disconnect")
    {
        $data->{call_duration} = ReadingsVal($fb, "call_duration", undef);
        $data->{finished} = gettimeofday();

        if($data->{last_event} =~ /^call|ring$/)
        {
            $data->{missed_call} = 1;
        }

        delete($data->{running_call});

        Log3 $name, 5, "FB_CALLLIST ($name) - processed disconnect event for call id $call_id";
    }

    $data->{last_event} = $event;

    # clean up the list
    FB_CALLLIST_cleanupList($hash);

    # save current list state to file/configDB
    FB_CALLLIST_saveList($hash);

    return undef if(IsDisabled($name));

    # inform about changes of current call index
    FB_CALLLIST_updateOneItemInFHEMWEB($hash,$data->{internal_index});

    # Update readings
    FB_CALLLIST_createReadings($hash);
}

############################################################################################################
#
#   Begin of helper functions
#
############################################################################################################


#####################################
# returns a hash reference to the data set of a specific running call
sub FB_CALLLIST_getDataReference($$)
{
    my ($hash, $call_id) = @_;

    my @result = grep {$hash->{helper}{DATA}{$_}{call_id} eq $call_id and defined($hash->{helper}{DATA}{$_}{running_call}) and $hash->{helper}{DATA}{$_}{running_call} == 1} keys %{$hash->{helper}{DATA}};

    return \%{$hash->{helper}{DATA}{$result[0]}} if(defined($result[0]));
    return undef;
}

#####################################
# cleans up the list from all unwanted entries
sub FB_CALLLIST_cleanupList($)
{
    my ($hash) = @_;

    my $name = $hash->{NAME};
    my $limit = int(AttrVal($name, "number-of-calls", 5));
    my $listtype = AttrVal($name, "list-type", "all");
    my $count = 0;
    my $index;

    my @list;

    if(exists($hash->{helper}{DATA}) and (scalar keys %{$hash->{helper}{DATA}}) > 0)
    {
        Log3 $name, 4, "FB_CALLLIST ($name) - cleaning up call list";

        # delete calls which not matched the configured list-type and number-of-calls
        if($listtype ne "all")
        {
            @list = grep { ($hash->{helper}{DATA}{$_}{direction} ne "incoming") or ($hash->{helper}{DATA}{$_}{direction} eq "incoming" and ++$count > $limit) } sort {$b <=> $a} keys %{$hash->{helper}{DATA}} if($listtype eq "incoming");

            @list = grep { ($hash->{helper}{DATA}{$_}{direction} ne "outgoing") or ($hash->{helper}{DATA}{$_}{direction} eq "outgoing" and ++$count > $limit) } sort {$b <=> $a} keys %{$hash->{helper}{DATA}} if($listtype eq "outgoing");

            @list = grep { ($hash->{helper}{DATA}{$_}{direction} eq "outgoing") or (!$hash->{helper}{DATA}{$_}{running_call} and not (((!$hash->{helper}{DATA}{$_}{missed_call} and AttrVal($name, "answMachine-is-missed-call", "0") eq "1" and $hash->{helper}{DATA}{$_}{internal_connection} =~ /^Answering_Machine/) or $hash->{helper}{DATA}{$_}{missed_call}) and not ++$count > $limit)) } sort {$b <=> $a} keys %{$hash->{helper}{DATA}} if($listtype eq "missed-calls");

            @list = grep { (not $hash->{helper}{DATA}{$_}{running_call}) and ++$count > $limit } sort {$b <=> $a} keys %{$hash->{helper}{DATA}} if($listtype eq "completed");

            @list = grep { (not $hash->{helper}{DATA}{$_}{running_call}) or ($hash->{helper}{DATA}{$_}{running_call} and ++$count > $limit)} sort {$b <=> $a} keys %{$hash->{helper}{DATA}} if($listtype eq "active");
        }
        else
        {
            @list = grep { ++$count > $limit } sort {$b <=> $a} keys %{$hash->{helper}{DATA}};
        }

        # delete calls which do not match the configured internal-number-filter
        if(exists($hash->{helper}{INTERNAL_FILTER}))
        {
            push @list, grep { not FB_CALLLIST_checkForInternalNumberFilter($hash, $hash->{helper}{DATA}{$_}{internal_number}) }  keys %{$hash->{helper}{DATA}};
        }

        # delete the collected list of unwanted calls
        foreach $index (@list)
        {
            Log3 $name, 5, "FB_CALLLIST ($name) - deleting old call $index";
            FB_CALLLIST_deleteItem($hash,$index);
        }

        FB_CALLLIST_deleteExpiredCalls($hash);
    }
    else
    {
         Log3 $name, 4, "FB_CALLLIST ($name) - list is empty. no cleanup needed";
    }
}

#####################################
# check if calls are expired and delete them
sub FB_CALLLIST_deleteExpiredCalls($;$)
{
    my ($hash, $save) = @_;

    if(ref($hash) ne "HASH")
    {
       ($hash, $save) = ($defs{$hash}, 1);
    }

    my $name = $hash->{NAME};
    my $expireCallSeconds = AttrVal($name, "expire-calls-after", 0);

    RemoveInternalTimer($name, "FB_CALLLIST_deleteExpiredCalls");

    if($expireCallSeconds !~ /^\d+(?:\.\d+)?$/)
    {
        if($expireCallSeconds =~ /^\s*(\d+(?:\.\d+)?)\s+minutes?\s*$/i)
        {
            $expireCallSeconds = $1 * 60;
        }
        elsif($expireCallSeconds =~ /^\s*(\d+(?:\.\d+)?)\s+hours?\s*$/i)
        {
            $expireCallSeconds = $1 * 3600;
        }
        elsif($expireCallSeconds =~ /^\s*(\d+(?:\.\d+)?)\s+days?\s*$/i)
        {
            $expireCallSeconds = $1 * 86400;
        }
        elsif($expireCallSeconds =~ /^\s*(\d+(?:\.\d+)?)\s+weeks?\s*$/i)
        {
            $expireCallSeconds = $1 * 86400 * 7;
        }
        elsif($expireCallSeconds =~ /^\s*(\d+(?:\.\d+)?)\s+months?\s*$/i)
        {
            $expireCallSeconds = $1 * 86400 * 30;
        }
        elsif($expireCallSeconds =~ /^\s*(\d+(?:\.\d+)?)\s+year?\s*$/i)
        {
            $expireCallSeconds = $1 * 86400 * 356;
        }
        else
        {
            $expireCallSeconds = 0;
        }
    }

    # delete expired calls if activated
    if($expireCallSeconds =~ /^\d+(?:\.\d+)?$/ and $expireCallSeconds > 0)
    {
        my @list = grep { !($hash->{helper}{DATA}{$_}{running_call}) and ((exists($hash->{helper}{DATA}{$_}{finished}) ? $hash->{helper}{DATA}{$_}{finished} : $_) < (gettimeofday - $expireCallSeconds)) } keys %{$hash->{helper}{DATA}};

        if(@list)
        {
            # delete the collected list of expired calls
            foreach my $index (@list)
            {
                Log3 $name, 5, "FB_CALLLIST ($name) - deleting expired call $index";

                FB_CALLLIST_deleteItem($hash, $index);
            }
        }

        my ($oldest) = sort {$b <=> $a} map {(exists($hash->{helper}{DATA}{$_}{finished}) ? $hash->{helper}{DATA}{$_}{finished} : $_) } grep { !$hash->{helper}{DATA}{$_}{running_call} } keys %{$hash->{helper}{DATA}};

        if(defined($oldest))
        {
            my $diff = $expireCallSeconds - (gettimeofday() - $oldest);

            if($diff > 0)
            {
                Log3 $name, 4, "FB_CALLLIST ($name) - oldest call $oldest expires in $diff seconds, scheduling timer...";
                InternalTimer(gettimeofday()+$diff+1, "FB_CALLLIST_deleteExpiredCalls", $name);
            }
        }

        # save current list state to file/configDB
        FB_CALLLIST_saveList($hash) if($save);
    }
}

#####################################
#  returns the icon depending on icon mapping
sub FB_CALLLIST_returnIcon($$$)
{
    my ($hash, $icon, $text) = @_;

    my $icon_name;

    my %standard = (

        "incoming.connected" => 'phone_ring_in@blue',
        "outgoing.connected" => 'phone_ring_out@green',

        "incoming.ring" => 'phone_ring@blue',
        "outgoing.ring" => 'phone_ring@green',

        "incoming.missed" => 'phone_missed_in@red',
        "outgoing.missed" => 'phone_missed_out@green',

        "incoming.done" => 'phone_call_end_in@blue',
        "outgoing.done" => 'phone_call_end_out@green',

        "incoming.tam" => 'phone_answering@blue'
    );

    $icon_name = $standard{$icon} if(exists($standard{$icon}));
    $icon_name = $hash->{helper}{ICON_MAP}{$icon} if(exists($hash->{helper}{ICON_MAP}{$icon}));

    my $result = FW_makeImage($icon_name);

    return $result if($result ne $icon_name);
    return $text;
}

#####################################
#  returns the call state of a specific call as icon or text
sub FB_CALLLIST_returnCallState($$;$)
{
    my ($hash, $index, $icons) = @_;

    return undef unless(exists($hash->{helper}{DATA}{$index}));

    my $data = $hash->{helper}{DATA}{$index};
    my $state;

    $icons = AttrVal($hash->{NAME}, "show-icons", 1) unless(defined($icons));

    if($data->{running_call})
    {
        if($data->{direction} eq "incoming" and $data->{last_event} eq "connect" )
        {
            $state = "=> [=]";
            $state = FB_CALLLIST_returnIcon($hash,"incoming.connected", $state) if($icons);
        }
        elsif($data->{direction} eq "incoming" and $data->{last_event} eq "ring")
        {
            $state = "=> ((o))";
            $state = FB_CALLLIST_returnIcon($hash,"incoming.ring", $state) if($icons);
        }
        elsif($data->{direction} eq "outgoing" and $data->{last_event} eq "connect" )
        {
            $state = "<= [=]";
            $state = FB_CALLLIST_returnIcon($hash,"outgoing.connected", $state) if($icons);
        }
        elsif($data->{direction} eq "outgoing" and $data->{last_event} eq "call")
        {
            $state = "<= ((o))";
            $state = FB_CALLLIST_returnIcon($hash,"outgoing.ring", $state) if($icons);
        }
    }
    else
    {
        if($data->{direction} eq "incoming" and ((not exists($data->{internal_connection}) ) or (exists($data->{internal_connection}) and not $data->{internal_connection} =~ /Answering_Machine/)))
        {
            $state = "=>".($data->{missed_call} ? " X" : "");
            $state = FB_CALLLIST_returnIcon($hash, "incoming.".($data->{missed_call} ? "missed" : "done"), $state) if($icons);
        }
        elsif($data->{direction} eq "incoming" and exists($data->{internal_connection}) and $data->{internal_connection} =~ /^Answering_Machine/)
        {
            $state = "=> O_O";
            $state = FB_CALLLIST_returnIcon($hash,"incoming.tam", $state) if($icons);
        }
        elsif($data->{direction} eq "outgoing")
        {
            $state = "<=".($data->{missed_call} ? " X" : "");
            $state = FB_CALLLIST_returnIcon($hash, "outgoing.".($data->{missed_call} ? "missed" : "done"), $state) if($icons);
        }
    }

    return $state;
}

#####################################
# FW_detailFn & FW_summaryFn handler for creating the html output in FHEMWEB
sub FB_CALLLIST_makeTable($$$$)
{
    my ($FW_wname, $devname, $room, $extPage) = @_;

    my $hash = $defs{$devname};

    return FB_CALLLIST_list2html($hash)
}

#####################################
# get the complete list as formated list items to display
sub FB_CALLLIST_getAllItemLines($)
{
    my ($hash) = @_;

    my $name = $hash->{NAME};
    my @list = FB_CALLLIST_createOrderedIndexList($hash);
    my @result;

    if(@list)
    {
        foreach my $index (@list)
        {
            my $line = FB_CALLLIST_index2line($hash, $index);
            push @result, $line if($line);
        }
    }

    return @result;
}

#####################################
# creates a line hash from index
sub FB_CALLLIST_index2line($$)
{
    my ($hash, $index) = @_;
    my $name = $hash->{NAME};

    if(exists($hash->{helper}{DATA}{$index}))
    {
        my $data = \%{$hash->{helper}{DATA}{$index}};
        my $count = FB_CALLLIST_getItemLineNumberFromIndex($hash,$index);

        return undef unless(defined($count)); # call should not be displayed

        my $old_locale = setlocale(LC_ALL);

        if(AttrVal($name, "language", "en") eq "de")
        {
            setlocale(LC_ALL, "de_DE.utf8");
        }
        else
        {
            setlocale(LC_ALL, "en_US.utf8");
        }
        my $line = {
                        index => $index,
                        line => $count,      # internal line identifier for JavaScript, must be present
                        row => $count,       # column "row" to display or not
                        state =>  FB_CALLLIST_returnCallState($hash, $index),
                        timestamp => FB_CALLLIST_strftime(AttrVal($name, "time-format-string", "%a, %d %b %Y %H:%M:%S"), localtime($index)),
                        name => ($data->{external_name} eq "unknown" ? "-" : $data->{external_name}),
                        number =>  ($data->{external_number} eq "unknown" ? "-" : $data->{external_number}),
                        external => ($data->{external_connection} ? ((exists($hash->{helper}{EXTERNAL_MAP}) and exists($hash->{helper}{EXTERNAL_MAP}{$data->{external_connection}})) ? $hash->{helper}{EXTERNAL_MAP}{$data->{external_connection}} : $data->{external_connection} ) : "-"),
                        internal => ((exists($hash->{helper}{INTERNAL_FILTER}) and exists($hash->{helper}{INTERNAL_FILTER}{$data->{internal_number}})) ? $hash->{helper}{INTERNAL_FILTER}{$data->{internal_number}} : $data->{internal_number} ),
                        connection => ($data->{internal_connection} ? ((exists($hash->{helper}{CONNECTION_MAP}) and exists($hash->{helper}{CONNECTION_MAP}{$data->{internal_connection}})) ? $hash->{helper}{CONNECTION_MAP}{$data->{internal_connection}} : $data->{internal_connection} ) : "-"),
                        duration => FB_CALLLIST_formatDuration($hash, $index),
                        image => FB_CALLLIST_getImagePathForNumber($hash, $data->{external_number})
                   };

        setlocale(LC_ALL, $old_locale);

        return $line;
    }

    return undef;
}

#####################################
# creates an array of data indices in order to display
sub FB_CALLLIST_createOrderedIndexList($)
{
    my ($hash) = @_;

    my $name = $hash->{NAME};
    my @list;

    if(exists($hash->{helper}{DATA}) and (scalar keys %{$hash->{helper}{DATA}}) > 0)
    {
        my $count = 0;

        @list = sort { (AttrVal($name, "list-order","descending") eq "descending") ? $b <=> $a : $a <=> $b } keys %{$hash->{helper}{DATA}};

        if(AttrVal($name, "list-type", "all") eq "missed-calls")
        {
            @list = grep { !$hash->{helper}{DATA}{$_}{running_call} } @list;
        }

        if(AttrVal($name, "list-type", "all") eq "completed")
        {
            @list = grep { !$hash->{helper}{DATA}{$_}{running_call} } @list;
        }
    }

    return @list;
}


#####################################
# get formated list items to display
sub FB_CALLLIST_getItemLineNumberFromIndex($$)
{
    my ($hash,$index) = @_;

    my $name = $hash->{NAME};
    my @list = FB_CALLLIST_createOrderedIndexList($hash);

    if(@list)
    {
        my $count = 0;

        foreach my $tmp (@list)
        {
            $count++;
            return $count if($tmp eq $index);
        }
    }

    return undef; # call should not be displayed
}

#####################################
# creating the call list as html string
sub FB_CALLLIST_list2html($)
{
    my ($hash) = @_;

    return undef if( !$hash );

    my $name = $hash->{NAME};
    my $alias = AttrVal($hash->{NAME}, "alias", $hash->{NAME});
    my $create_readings = AttrVal($hash->{NAME}, "create-readings","0");
    my $td_style = 'style="padding-left:6px;padding-right:6px;"';
    my $line;

    my $ret .= '<table class="fbcalllist-container">';

    if(AttrVal($name, "no-heading", "0") eq "0" and defined($FW_ME) and defined($FW_subdir))
    {
        $ret .=" <tr><td>";
        $ret .= '<div class="devType"><a href="'.$FW_ME.$FW_subdir.'?detail='.$name.'">'.$alias.'</a>'.(IsDisabled($name) ? " (disabled)" : "").'</div>' unless($FW_webArgs{"detail"});
        $ret .= "</td></tr>";
    }

    $ret .= "<tr><td>";
    $ret .= '<div class="fhemWidget" informId="'.$name.'" cmd="" arg="fbcalllist" dev="'.$name.'">'; # div tag to support inform updates
    $ret .= '<table class="block wide fbcalllist"'.((AttrVal($name, "disable", "0") eq "3") ? ' style="display:none;"' : '').'>';

    $ret .= FB_CALLLIST_returnOrderedHTMLOutput($hash, FB_CALLLIST_returnTableHeader($hash), 'class="fbcalllist header"'.((AttrVal($name, "no-table-header", "0") eq "1") ? ' style="display:none;"' : ''),'', 1);

    if(AttrVal($name,'disable',"0") eq "2")
    {
        my $string = '<div style="color:#ff8888;"><i>'.((AttrVal($name, "language", "en") eq "de") ? "deaktiviert" : "disabled").'</i></div>';

        my @columns = split(",",AttrVal($name, "visible-columns", $hash->{helper}{DEFAULT_COLUMN_ORDER}));
        my $additional_columns = scalar(@columns);

        $ret .= '<tr align="center" name="empty"><td style="padding:10px;" colspan="'.$additional_columns.'">'.$string.'</td></tr>';
    }
    else
    {
        my @item_list = FB_CALLLIST_getAllItemLines($hash);

        if(@item_list > 0)
        {
            foreach $line (@item_list)
            {
                $ret .= FB_CALLLIST_returnOrderedHTMLOutput($hash, $line, 'number="'.$line->{line}.'" index="'.$line->{index}.'" class="fbcalllist item '.($line->{line} % 2 == 1 ? "odd" : "even").'"', 'class="fbcalllist cell" '.$td_style);
            }
        }
        else
        {
            my $string = ((AttrVal($name, "language", "en") eq "de") ? "leer" : "empty");

            my @columns = split(",",AttrVal($name, "visible-columns", $hash->{helper}{DEFAULT_COLUMN_ORDER}));
            my $additional_columns = scalar(@columns);

            $ret .= '<tr align="center" name="empty"><td style="padding:10px;" colspan="'.$additional_columns.'"><i>'.$string.'</i></td></tr>';
        }
    }

    $ret .= "</table></div>";
    $ret .= "</td></tr>";

    $ret .= "</table>";

    return $ret;
}


#####################################
# generate all readings for the call list
sub FB_CALLLIST_createReadings($)
{
    my ($hash) = @_;

    return undef if( !$hash );

    my $name = $hash->{NAME};
    my $create_readings = AttrVal($hash->{NAME}, "create-readings","0");

    return undef unless($create_readings);

    my @item_list = FB_CALLLIST_getAllItemLines($hash);

    readingsBeginUpdate($hash);

    if(@item_list > 0)
    {
        foreach my $line (@item_list)
        {
            FB_CALLLIST_createReadingsForItem($hash, $line);
        }
    }
    
    readingsBulkUpdate($hash, "numberOfCalls", scalar @item_list, 1);
    
    my %counters = ("all" => 0, "incoming" => 0, "outgoing" => 0, "missed-calls" => 0, "active" => 0, "completed" => 0 ); 
            
    foreach my $line (@item_list)
    {
        my $item = $hash->{helper}{DATA}{$line->{"index"}};
        
        $counters{"all"}++;
        $counters{"incoming"}++ if($item->{direction} eq "incoming");
        $counters{"outgoing"}++ if($item->{direction} eq "outgoing");
        $counters{"missed-calls"}++ if(($item->{direction} eq "incoming") and ($item->{"missed_call"} or (AttrVal($name, "answMachine-is-missed-call", "0") eq "1" and $item->{"internal_connection"} =~ /^Answering_Machine/)));
        $counters{"active"}++ if($item->{running_call});
        $counters{"completed"}++ unless($item->{running_call});
    }

    foreach my $counter (keys(%counters))
    {
        readingsBulkUpdate($hash, "count-$counter", $counters{$counter});
    }

    # delete old readings
    my @delete_readings;

    for my $reading (grep { /^(\d+)-/ and ($1 > @item_list) } keys %{$hash->{READINGS}})
    {
        readingsBulkUpdate($hash, $reading, "");
        readingsDelete($hash, $reading) ;
    }

    readingsEndUpdate($hash, 1);

    return undef;
}

#####################################
# format duration in seconds into hh:mm:ss
sub FB_CALLLIST_formatDuration($$)
{
    my ($hash, $index) = @_;

    my $data = \%{$hash->{helper}{DATA}{$index}};

    if($data->{running_call})
    {
        if(AttrVal($hash->{NAME}, "language", "en") eq "de")
        {
            return "<i>l&auml;uft</i>";
        }
        else
        {
            return "<i>ongoing</i>";
        }
    }

    my $hour = int($data->{call_duration} / (60 * 60));
    my $minute = ($data->{call_duration} / 60) % 60;
    my $seconds = int($data->{call_duration} % 60);

    return "-"  if($data->{missed_call});
    return sprintf("%02d:%02d:%02d", $hour, $minute, $seconds);
}

#####################################
# save the current call list to file or configDB
sub FB_CALLLIST_saveList($)
{
    my ($hash) = @_;
    my $name = $hash->{NAME};

    if(exists($hash->{helper}{DATA}))
    {
        Log3 $name, 5, "FB_CALLLIST ($name) - start dumping of list to file";

        my $dumper = Data::Dumper->new([$hash->{helper}{DATA}], [qw($hash->{helper}{DATA})] );
        $dumper->Purity(1);
        $dumper->Terse(0);

        my $dump = $dumper->Dump;

        eval { require Compress::Zlib; };

        unless($@)
        {
            Log3 $name, 5, "FB_CALLLIST ($name) - found Compress::Zlib module, compressing dump";
            $dump = Compress::Zlib::compress($dump);
            $dump = "compressed:".encode_base64($dump, "");
        }
        else
        {
            Log3 $name, 5, "FB_CALLLIST ($name) - unable to load Compress::Zlib module: $@";
            Log3 $name, 5, "FB_CALLLIST ($name) - using just plain base64 encoding for dump";
            $dump = encode_base64($dump, "");
        }

        Log3 $name, 5, "FB_CALLLIST ($name) - saving list dump: ".$dump;

        my $err = setKeyValue("FB_CALLLIST-$name", $dump);

        Log3 $name, 3, "FB_CALLLIST ($name) - error while saving the current call list: $err" if(defined($err));
    }
    else
    {
        my $err = setKeyValue("FB_CALLLIST-$name", undef);

        Log3 $name, 3, "FB_CALLLIST ($name) - error while saving the current call list: $err" if(defined($err));
    }
}

#####################################
# load the call list from file or configDB
sub FB_CALLLIST_loadList($)
{
    my ($hash) = @_;
    my $name = $hash->{NAME};

    Log3 $name, 5, "FB_CALLLIST ($name) - loading old call list from file";

    delete($hash->{helper}{DATA});

    my ($err, $dump) = getKeyValue("FB_CALLLIST-$name");

    if(defined($err))
    {
        Log3 $name, 3, "FB_CALLLIST ($name) - error while loading the old call list state: $err";
        return undef;
    }

    if(defined($dump))
    {
        if($dump =~ /^compressed:(.+)$/)
        {
            Log3 $name, 5, "FB_CALLLIST ($name) - found compressed list dump in file";

            $dump = $1;

            eval { require Compress::Zlib; };

            unless($@)
            {
                $dump = decode_base64($dump);
                $dump = Compress::Zlib::uncompress($dump);
            }
            else
            {
                 Log3 $name, 3, "FB_CALLLIST ($name) - unable to load module Compress::Zlib to unpack compressed old call list: $@";
                 return undef;
            }
        }
        else
        {
            $dump = decode_base64($dump);
        }

        Log3 $name, 5, "FB_CALLLIST ($name) - importing list...\n$dump";

        eval($dump);

        Log3 $name, 3, "FB_CALLLIST ($name) - error while importing old call list state: $@" if($@);
    }
    else
    {
        Log3 $name, 5, "FB_CALLLIST ($name) - no list found for restoring";
    }
}

#####################################
# produce a HTML <tr>-Output for a specific data set depending on visible-columns setting
sub FB_CALLLIST_returnOrderedHTMLOutput($$$$;$)
{

    my ($hash, $line, $tr_additions, $td_additions,$is_header) = @_;

    my $name = $hash->{NAME};

    my @order = split(",", AttrVal($name, "visible-columns",$hash->{helper}{DEFAULT_COLUMN_ORDER}));

    my @ret = ();

    if(defined(my $cmd = AttrVal($name, "number-cmd", undef)) and $line->{number} =~/\d$/)
    {
        $cmd =~ s/\$NUMBER/$line->{number}/g;

        $line->{number} = '<a href=\'#\' onclick="FW_cmd(FW_root+\'?XHR=1&cmd='.urlEncode($cmd).'\');return false;">'.$line->{number}."</a>";
    }
    

    push @ret, '<tr align="center" '.$tr_additions.'>';

    foreach my $col (@order)
    {
        if($col eq "image")
        {
            my $content;
            
            if($is_header)
            {   
                $content = $line->{$col};
            }
            else
            {
                my $url = FB_CALLLIST_generateImageUrl($hash, $line->{$col});
                $content = (defined($url) ? '<img style="max-height:3em;margin:0.2em;" src="'.$url.'">' : "-" );
            }
            
            push @ret, '<td name="'.$col.'" '.$td_additions.'>'.$content.'</td>';
            next;
        }
            
        push @ret, '<td name="'.$col.'" '.$td_additions.'>'.$line->{$col}.'</td>' if(defined($line->{$col}));
    }

    return join("",@ret)."</tr>";
}

#####################################
# produce a JSON Output for a specific data set depending on visible-columns setting
sub FB_CALLLIST_returnOrderedJSONOutput($$)
{
    my ($hash,$line) = @_;

    my $name = $hash->{NAME};

    my @order = split(",", AttrVal($name, "visible-columns",$hash->{helper}{DEFAULT_COLUMN_ORDER}));

    my @ret = ();

    if(defined(my $cmd = AttrVal($name, "number-cmd", undef)) and $line->{number} =~/\d$/)
    {
        $cmd =~ s/\$NUMBER/$line->{number}/g;

        $line->{number} = '<a href=\'#\' onclick="FW_cmd(FW_root+\'?XHR=1&cmd='.urlEncode($cmd).'\');return false;">'.$line->{number}."</a>";
    }
    
    $line->{image} = FB_CALLLIST_generateImageUrl($hash, $line->{image});

    push @ret, '"line":"'.$line->{line}.'"';

    foreach my $col (@order)
    {
        if($line->{$col})
        {
            my $val = $line->{$col};
            $val =~ s,",\\",g;
            push @ret, '"'.$col.'":"'.$val.'"';
        }
    }

    return "{".join(",",@ret)."}";
}

#####################################
# generate Readings for all list entries
sub FB_CALLLIST_createReadingsForItem($$)
{
    my ($hash,$line) = @_;

    my $name = $hash->{NAME};

    my %line_tmp = %{$line};

    my @order = split(",", AttrVal($name, "visible-columns",$hash->{helper}{DEFAULT_COLUMN_ORDER}));

    $line_tmp{state} = FB_CALLLIST_returnCallState($hash, $line->{index}, 0);

    foreach my $col (@order)
    {
        readingsBulkUpdate($hash, $line_tmp{line}."-$col", $line_tmp{$col}) if($line->{$col});
    }
}

#####################################
# Check, if a given internal number matches the configured internal-number-filter (if set). returns true if number matches
sub FB_CALLLIST_checkForInternalNumberFilter($$)
{
    my ($hash, $line_number) = @_;
    my $name = $hash->{NAME};

    if(exists($hash->{helper}{INTERNAL_FILTER}))
    {
        Log3 $name, 5, "FB_CALLLIST ($name) - internal-number-filter is defined, checking if internal number $line_number is allowed";

        if(defined($line_number) and not exists($hash->{helper}{INTERNAL_FILTER}{$line_number}))
        {
            Log3 $name, 5, "FB_CALLLIST ($name) - internal number $line_number does not match the current internal-number-filter: ".Dumper($hash->{helper}{INTERNAL_FILTER});
            return undef;
        }
        else
        {
            Log3 $name, 5, "FB_CALLLIST ($name) - call passed the internal-number-filter. proceeding...";
        }
    }

    return 1;
}

#####################################
# delete an item from the list and update FHEMWEB clients
sub FB_CALLLIST_deleteItem($;$)
{
    my ($hash, $index) = @_;

    my $name = $hash->{NAME};

    delete($hash->{helper}{DATA}{$index}) if($index);

    if(FB_CALLLIST_createOrderedIndexList($hash))
    {
        FW_directNotify($name, '{"action":"delete","index":"'.$index.'"}', 1) if(defined($FW_ME) and $index);
    }
    else
    {
        Log3 $name, 5, "FB_CALLLIST ($name) - list is empty, sending a clear command to all FHEMWEB clients";

        # inform all FHEMWEB clients about empty list
        my @columns = split(",",AttrVal($name, "visible-columns", $hash->{helper}{DEFAULT_COLUMN_ORDER}));
        my $additional_columns = scalar(@columns);
        my $string;

        if(AttrVal($name, "language", "en") eq "de")
        {
            $string = "leer";
        }
        else
        {
            $string = "empty";
        }

        FW_directNotify($name, '{"action":"clear","content":"'.$string.'"}', 1);
    }
}


#####################################
# update the call list of all connected FHEMWEB clients via inform mechanism
sub FB_CALLLIST_updateFhemWebClients($)
{
    my ($hash) = @_;
    my $name = $hash->{NAME};

    return undef unless($init_done);

    if(IsDisabled($name))
    {
        my $string = "<div style='color:#ff8888;'><i>".((AttrVal($name, "language", "en") eq "de") ? "deaktiviert" : "disabled").'</i></div>';

        if(AttrVal($name,"disable","0") eq "2")
        {
            FW_directNotify($name, '{"action":"clear","content":"'.$string.'"}', 1);
        }
        elsif(AttrVal($name,"disable","0") eq "3")
        {
            FW_directNotify($name, '{"action":"hide"}', 1);
        }
    }
    else
    {
        if(my @list = FB_CALLLIST_getAllItemLines($hash))
        {
            Log3 $name, 5, "FB_CALLLIST ($name) - inform all FHEMWEB clients";

            # inform all FHEMWEB clients about changes
            foreach my $line (@list)
            {
                my $json = FB_CALLLIST_returnOrderedJSONOutput($hash, $line);
                FW_directNotify($name, '{"action":"update","index":"'.$line->{index}.'","order":"'.AttrVal($name, "list-order","descending").'","item":'.$json.'}', 1);
            }
        }
        else
        {
            FB_CALLLIST_deleteItem($hash);
        }

        FW_directNotify($name, '{"action":"show"}', 1);
    }
}

#####################################
# update one particular item of the call list of all connected FHEMWEB clients via inform mechanism
sub FB_CALLLIST_updateOneItemInFHEMWEB($$)
{
    my ($hash, $index) = @_;
    my $name = $hash->{NAME};

    my $line = FB_CALLLIST_index2line($hash,$index);

    return undef unless($line); # abort if call should not be displayed (e.g. attr list-type = "completed")

    my $json = FB_CALLLIST_returnOrderedJSONOutput($hash, $line);

    FW_directNotify($name, '{"action":"update","index":"'.$index.'","order":"'.AttrVal($name, "list-order","descending").'","item":'.$json.'}', 1);

    return undef;
}

#####################################
# returns the filename of a corresponding contact image if exist.
sub FB_CALLLIST_getImagePathForNumber($$)
{
    my ($hash, $number) = @_;
    my $name = $hash->{NAME};
 
    my $local_path = AttrVal($name,"contactImageDirectory", undef);
    
    return undef unless(defined($local_path));
    return undef if($number eq "-");
    
    $local_path =~ s,/+$,,;
    
    opendir(DIR, $local_path) or return undef;

    while (my $file = readdir(DIR))
    {
        next if($file =~ /^\./);
        next unless(-f "$local_path/$file");
        next unless($file =~ /\.(?:gif|jpg|jpe|jpeg|png|bmp)$/i);
        next unless($file =~ /^$number\./);

        return $file;
    }
    
    return AttrVal($name, "contactDefaultImage", undef);
}

#####################################
# generates a data URL for a specific image file
sub FB_CALLLIST_generateImageUrl($$)
{
    my ($hash, $file) = @_;
    my $name = $hash->{NAME};
    
    return undef unless(defined($file));
    
    my $local_path = AttrVal($name,"contactImageDirectory", undef);
    
    return undef unless(defined($local_path));
    return undef if ($file eq "-");
    
    my $suffix = lc((split(/\./, $file))[-1]);
    
    $suffix = "jpeg" if($suffix =~ /^(?:jpg|jpe)$/);
    
    $local_path =~ s,/+$,,;    
    
    my ($err, @content) = FileRead({FileName => $local_path."/".$file, ForceType=> "file"});
    
    if($err)
    {
        Log3 $name, 3 , "FB_CALLIST ($name) - unable to load contact image: $err";
        return undef;
    }
    
    my $base64 = encode_base64(join("\n", @content), "");
    
    return "data:image/$suffix;base64,$base64";
}


#####################################
# returns the table header in the configured language
sub FB_CALLLIST_returnTableHeader($)
{
    my ($hash) = @_;
    my $name = $hash->{NAME};
    my $line;

    if(AttrVal($name, "language", "en") eq "de")
    {
        $line = {
            row => "",
            state => "Status",
            timestamp => "Zeitpunkt",
            name => "Name",
            image => "Bild",
            number => "Rufnummer",
            internal => "Intern",
            external => "Extern",
            connection => "Via",
            duration => "Dauer"
        };
    }
    else
    {
        $line = {
            row => "",
            state => "State",
            timestamp => "Timestamp",
            name => "Name",
            image => "Image",
            number => "Number",
            internal => "Internal",
            external => "External",
            connection => "Via",
            duration => "Duration"
        };
    }

    return $line;
}

#####################################
# In newer perl versions (>=5.22) POSIX::strftime() returns special chars in ISO-8859 instead of active locale (see: https://forum.fhem.de/index.php/topic,85132.msg777667.html#msg777667 )
sub FB_CALLLIST_strftime(@)
{
    my $string = POSIX::strftime(@_);

    $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;

    return $string;
}


1;

=pod
=item helper
=item summary    creates a call history list, based on a FB_CALLMONITOR definition
=item summary_DE erzeugt eine Anrufliste basierend auf einer FB_CALLMONITOR-Definition
=begin html

<a name="FB_CALLLIST"></a>
<h3>FB_CALLLIST</h3>
<ul>
  The FB_CALLLIST module creates a call history list by processing events of a <a href="#FB_CALLMONITOR">FB_CALLMONITOR</a> definition.
  It logs all calls and displays them in a historic table.
  <br><br>
  You need a defined FB_CALLMONITOR instance where you can attach FB_CALLLIST to process the call events.<br><br>
  Depending on your configuration the status will be shown as icons or as text. You need to have the openautomation icon set configured in your corresponding FHEMWEB instance (see FHEMWEB attribute <a href="#iconPath">iconPath</a>).
  <br><br>
  The icons have different colors.<br><br>
  <ul>
  <li><font color="blue"><b>blue</b></font> - incoming call (active or finished)</li>
  <li><font color="green"><b>green</b></font> - outgoing call (active or finished)</li>
  <li><font color="red"><b>red</b></font> - missed incoming call</li>
  </ul>
  <br>
  If you use no icons (see <a href="#FB_CALLLIST_show-icons">show-icons</a>) the following states will be shown:<br><br>
  <ul>
    <li><code><b>&lt;= ((o))</b></code></td><td> - outgoing call (ringing) - icon: <code><b>outgoing.ring</b></code></li>
    <li><code><b>=&gt; ((o))</b></code></td><td> - incoming call (ringing) - icon: <code><b>incoming.ring</b></code></li>
    <br>
    <li><code><b>&lt;= [=]</b></code></td><td> - outgoing call (currently active) - icon: <code><b>outgoing.connected</b></code></li>
    <li><code><b>=&gt; [=]</b></code></td><td> - incoming call (currently active) - icon: <code><b>incoming.connected</b></code></li>
    <br>
    <li><code><b>&lt;= X</b></code></td><td> - outgoing unsuccessful call (nobody picked up) - icon: <code><b>outgoing.missed</b></code></li>
    <li><code><b>=&gt; X</b></code></td><td> - incoming unsuccessful call (missed call) - icon: <code><b>incoming.missed</b></code></li>
    <br>
    <li><code><b>=&gt; O_O</b></code></td><td> - incoming finished call recorded on answering machine - icon: <code><b>incoming.tam</b></code></li>
    <br>
    <li><code><b>&lt;=</b></code></td><td> - outgoing finished call - icon: <code><b>outgoing.done</b></code></li>
    <li><code><b>=&gt;</b></code></td><td> - incoming finished call - icon: <code><b>incoming.done</b></code></li>
  </ul>
  <br>
  The default icon mapping for all states can be changed by the corresponding attribute.
  <br>
  <br>

  <a name="FB_CALLLIST_define"></a>
  <b>Define</b>
  <ul>
    <code>define &lt;name&gt; FB_CALLLIST &lt;FB_CALLMONITOR name&gt;</code><br>
  </ul>
  <br>
  <a name="FB_CALLLIST_set"></a>
  <b>Set</b><br>
  <ul>
  <li><b>clear</b> - clears the list completely</li>
  <li><b>removeItem &lt;index&gt;</b> - removes a specific item from the list (row number)</li>
  </ul>
  <br>

  <a name="FB_CALLLIST_get"></a>
  <b>Get</b><br>
  <ul>
  No get commands implemented.
  </ul>
  <br>

  <a name="FB_CALLLIST_attr"></a>
  <b>Attributes</b><br><br>
  <ul>
    <li><a href="#do_not_notify">do_not_notify</a></li>
    <li><a href="#readingFnAttributes">readingFnAttributes</a></li>

    <br>

    <li><a name="FB_CALLLIST_answMachine-is-missed-call">answMachine-is-missed-call</a> 0,1</li>
    If activated, a incoming call, which is answered by an answering machine, will be treated as a missed call. This is only relevant if <a href="#FB_CALLLIST_list-type">list-type</a> is set to "missed-call" or <a href="#FB_CALLLIST_create-readings">create-readings</a> is set.
    <br><br>
    Possible values: 0 =&gt; disabled, 1 =&gt; enabled (successful answering machine calls will be treated as "missed call").<br>
    Default Value is 0 (disabled)<br><br>

    <li><a name="FB_CALLLIST_connection-mapping">connection-mapping</a> &lt;hash&gt;</li>
    Defines a custom mapping of connection names to custom values. The mapping is performed in a hash table.<br><br>
    e.g.<br>
    <ul>
    <code>attr &lt;name&gt; connection-mapping {'DECT_1' =&gt; 'Mobile Kitchen', 'FON1' =&gt; 'Fax'}</code>
    </ul><br>
    The mapped name will be displayed in the table instead of the original value from FB_CALLMONITOR.
    <br><br>
    Default Value: <i>empty</i> (no mapping is performed)
    <br><br>

    <li><a name="FB_CALLLIST_create-readings">create-readings</a> 0,1</li>
    If enabled, for all visible calls in the list, readings and events will be created. It is recommended to set the attribute <a href="#event-on-change-reading">event-on-change-reading</a> to <code>.*</code> (all readings), to reduce the amount of generated readings for certain call events.<br><br>
    Possible values: 0 =&gt; no readings will be created, 1 =&gt; readings and events will be created.<br>
    Default Value is 0 (no readings will be created)
    <br><br>

    <li><a name="FB_CALLLIST_contactImageDirectory">contactImageDirectory</a> &lt;directory&gt;</li>
    If set, FB_CALLLIST will use this directory to display a contact image for each call.
    This image will be shown in the column "image", which needs to be explicit configured via attribute <a href="#FB_CALLLIST_visible-columns">visible-columns</a>.
    If this directory contains a file with the external number as filename (e.g. "0123456789.jpg" or "0345678901.gif"), it will be displayed in the call list.
    <br><br>
    Supported formats are: JPEG, GIF, PNG, BMP
    <br><br>
    By default, no directory is set and therefore no images will be displayed
    <br><br>
    
    <li><a name="FB_CALLLIST_contactImageDirectory">contactDefaultImage</a> &lt;filename&gt;</li>
    If contact images are configured (via attribute <a href="#FB_CALLLIST_contactImageDirectory">contactImageDirectory</a>) and there is no image file available or the external number is unknown,
    FB_CALLLIST will use this file (e.g. <code>unkown.jpg</code>) as contact image in case there is no image for the external number available or the external number is unknown.
    The file must be located in the directory configured via attribute <a href="#FB_CALLLIST_contactImageDirectory">contactImageDirectory</a>.
    <br><br>
    If not configured, no images will be shown for such calls.
    <br><br>
    
    <li><a name="FB_CALLLIST_disable">disable</a> 0,1,2,3</li>
    Optional attribute to disable the call list. When disabled, call events will not be processed and the list wouldn't be updated accordingly. Depending on the value, the call list can
    <br><br>
    Possible values:<ul>
    <li>0 =&gt; FB_CALLLIST is activated, proccess events and updates the table</li>
    <li>1 =&gt; Events will NOT be processed. table will NOT be updated (stays as it is)</li>
    <li>2 =&gt; Events will NOT be processed. table just shows "disabled" (no items)</li>
    <li>3 =&gt; Events will NOT be processed. table will NOT be shown entirely</li>
    </ul><br>
    Default Value is 0 (activated)<br><br>

    <li><a name="FB_CALLLIST_disabledForIntervals">disabledForIntervals</a> HH:MM-HH:MM HH:MM-HH:MM...</li>
    Optional attribute to disable event processing and updates of the call list during a specific time interval. The attribute contains a space separated list of HH:MM tupels.
    If the current time is between any of these time specifications, the callist will be disabled and no longer updated.
    Instead of HH:MM you can also specify HH or HH:MM:SS.
    <br><br>To specify an interval spawning midnight, you have to specify two intervals, e.g.:
    <pre>23:00-24:00 00:00-01:00</pre>
    Default Value is <i>empty</i> (no intervals defined, calllist is always active)<br><br>

    <li><a name="FB_CALLLIST_processEventsWhileDisabled">processEventsWhileDisabled</a> 0,1</li>
    If enabled, events where still be processed, even FB_CALLLIST is disabled (see <a href="#FB_CALLLIST_disable">disable</a> and <a href="#FB_CALLLIST_disabledForIntervals">disabledForIntervals</a>). So after re-enabling FB_CALLLIST, all calls during disabled state are completely available.
    <br><br>
    Possible values: 0 =&gt; no event processing when FB_CALLIST is disabled, 1 =&gt; events are still processed, even FB_CALLLIST is disabled<br>
    Default Value is 0 (no event processing when disabled)<br><br>

    <li><a name="FB_CALLLIST_expire-calls-after">expire-calls-after</a> &lt;timeframe&gt;</li>
    Optional attribute to automatically delete finished calls which are older than a given timeframe. If a finished call is older than this timeframe, it will be deleted from the list.
    <br><br>A timeframe can be specified as follows:
    <ul>
    <li>as minutes: <code>1 minute</code> or <code>30 minutes</code></li>
    <li>as hours: <code>1 hour</code> or <code>12 hours</code></li>
    <li>as days: <code>1 day</code> or <code>5 days</code></li>
    <li>as months: <code>1 month</code> or <code>6 months</code> (in this case one month is equal to 30 days)</li>
    <li>as years: <code>1 year</code> or <code>2 years</code> (in this case one year is equal to 365 days)</li>
    </ul>
    <br>
    <b>IMPORTANT:</b> In this case, the ending time of each call is checked, not the beginning time.<br><br>

    If no unit is given, the given number ist interpreted as seconds. Float values can also be used (e.g. <code>0.5 day</code>).
    The value <code>0</code> means no expiry of calls, so no calls will be deleted because of expiry.<br><br>
    Default Value is 0 (no calls will be deleted because of expiry)<br><br>

    <li><a name="FB_CALLLIST_external-mapping">external-mapping</a> &lt;hash&gt;</li>
    Defines a custom mapping of external connection values (reading: external_connection) to custom values. The mapping is performed in a hash table.<br><br>
    e.g.<br>
    <ul>
    <code>attr &lt;name&gt; external-mapping {'ISDN' =&gt; 'Fixed Network', 'SIP0' =&gt; 'Operator A', 'SIP1' =&gt; 'Operator B'}</code>
    </ul><br>

    <li><a name="FB_CALLLIST_icon-mapping">icon-mapping</a> &lt;hash&gt;</li>
    Defines a custom mapping of call states to custom icons. The mapping is performed in a hash table.<br><br>
    e.g.<br>
    <ul>
    <code>attr &lt;name&gt; icon-mapping {'incoming.connected' =&gt; 'phone_ring_in@yellow', 'outgoing.missed' =&gt; 'phone_missed_out@red'}</code>
    </ul><br>
    The mapped name will be displayed in the table instead of the original value from FB_CALLMONITOR. If you use SVG-based icons, you can set the desired color as name or HTML color code via an optional "@<i>color</i>".
    <br><br>
    Possible values and their default icon are:<br><br>
    <ul>
    <li><b>incoming.ring</b> =&gt; phone_ring@blue</li>
    <li><b>outgoing.ring</b> =&gt; phone_ring@green</li>
    <li><b>incoming.connected</b> =&gt; phone_ring_in@blue</li>
    <li><b>outgoing.connected</b> =&gt; phone_ring_in@green</li>

    <li><b>incoming.missed</b> =&gt; phone_missed_in@red</li>
    <li><b>outgoing.missed</b> =&gt; phone_missed_out@green</li>
    <li><b>incoming.done</b> =&gt; phone_call_end_in@blue</li>
    <li><b>outgoing.done</b> =&gt; phone_call_end_out@green</li>
    <li><b>incoming.tam</b> =&gt; phone_answering@blue</li>
    </ul>
    <br><br>
    Default Value: <i>empty</i> (no mapping is performed)<br><br>

    <li><a name="FB_CALLLIST_internal-number-filter">internal-number-filter</a> &lt;hash&gt;</li>
    This attribute accepts a list of comma seperated internal numbers for
    filtering incoming or outgoing calls by a specific list of internal numbers
    or a hash for filtering and mapping numbers to text.<br>
    <br>
    e.g.<br>
    <ul>
      <code>attr &lt;name&gt; internal-number-filter 304050,304060<br><br>
      attr &lt;name&gt; internal-number-filter {'304050' =&gt; 'business', '304060' =&gt; 'private'}</code>
    </ul>
    <br><b>Important:</b> Depending on your provider, the internal number can contain a location area code.
    The internal-number-filter must contain the same number as it is displayed in the call list.
    This can be with or without location area code depending on your provider.
    <br><br>
    If this attribute is set, only the configured internal numbers will be shown in the list. All calls which are not taken via the configured internal numbers, were not be shown in the call list.
    <br><br>
    Default Value: <i>empty</i> (all internal numbers should be used, no exclusions and no mapping is performed)
    <br><br>

    <li><a name="FB_CALLLIST_language">language</a> en,de</li>
    Defines the language of the table header, some keywords and the timestamp format. You need to have the selected locale installed and available in your operating system.<br><br>
    Possible values: en =&gt; English , de =&gt; German<br>
    Default Value is en (English)<br><br>

    <li><a name="FB_CALLLIST_list-type">list-type</a> all,incoming,outgoing,missed-calls,completed,active</li>
    Defines what type of calls should be displayed in the list.<br><br>
    Default Value is "all"<br><br>

    <li><a name="FB_CALLLIST_list-order">list-order</a> descending,ascending</li>
    Defines whether the newest call should be on top of the list (descending) or on the bottom of the list (ascending).<br><br>
    Default Value is descending (first call at top of the list)<br><br>

    <li><a name="FB_CALLLIST_no-heading">no-heading</a> 0,1</li>
    If activated the headline with a link to the detail page of the current definition will be hidden.<br><br>
    Possible values: 0 =&gt; the heading line will be shown , 1 =&gt; the heading line will not be shown<br>
    Default Value is 0 (the heading line will be shown)<br><br>

    <li><a name="FB_CALLLIST_no-table-header">no-table-header</a> 0,1</li>
    If activated the table header containing the name of each column for the current definition will be hidden.<br><br>
    Possible values: 0 =&gt; the table header will be shown , 1 =&gt; the table header will not be shown<br>
    Default Value is 0 (the table header will be shown)<br><br>

    <li><a name="FB_CALLLIST_number-cmd">number-cmd</a> &lt;command&gt;</li>
    Can be set, to execute a specific FHEM command, when clicking on a number in the list. The value can be any valid FHEM command or Perl code (in curly brackets: { ... } ).
    The placeholder <code>$NUMBER</code> will be replaced with the current external number of each row.
    <br><br>
    This can be used for example to initiate a call to this number.
    e.g.:<br><br>
    <ul>
    <li><code>set FRITZBOX call $NUMBER</code></li>
    <li><code>{dialNumber("$NUMBER")}</code></li>
    </ul>
    <br>
    If not set, no link will be shown in the list.<br><br>

    <li><a name="FB_CALLLIST_number-of-calls">number-of-calls</a> 1..40</li>
    Defines the maximum number of displayed call entries in the list.<br><br>
    Default Value is 5 calls<br><br>

    <li><a name="FB_CALLLIST_show-icons">show-icons</a> 0,1</li>
    Normally the call state is shown with icons (used from the openautomation icon set).
    You need to have openautomation in your iconpath attribute of your appropriate FHEMWEB definition to use this icons.
    If you don't want to use icons you can deactivate them with this attribute.<br><br>
    Possible values: 0 =&gt; no icons , 1 =&gt; use icons<br>
    Default Value is 1 (use icons)<br><br>

    <li><a name="FB_CALLLIST_time-format-string">time-format-string</a> &lt;string&gt;</li>
    Defines a format string which should be used to format the timestamp values.
    It contains several placeholders for different elements of a date/time.
    The possible values are standard POSIX strftime() values. Common placeholders are:<br><br>
    <ul>
    <li><code>%a</code> - The abbreviated weekday name</li>
    <li><code>%b</code> - The abbreviated month name</li>
    <li><code>%S</code> - The second as a decimal number</li>
    <li><code>%M</code> - The minutes as a decimal number</li>
    <li><code>%H</code> - The hours as a decimal number</li>
    <li><code>%d</code> - The day of the month as a decimal number</li>
    <li><code>%m</code> - The month as a decimal number</li>
    <li><code>%Y</code> - The year as a decimal number including the century.</li>
    </ul><br>
    There are further placeholders available.
    Please consult the manpage of <code>strftime()</code> or the documentation of your perl interpreter to find out more.
    <br><br>
    Default value is "%a, %d %b %Y %H:%M:%S" ( = "Sun, 07 Jun 2015 12:50:09")<br><br>

    <li><a name="FB_CALLLIST_visible-columns">visible-columns</a> row,state,timestamp,image,name,number,internal,external,connection,duration</li>
    Defines the visible columns, as well as the order in which these columns are displayed in the call list (from left to right).
    Not all columns must be displayed, you can select only a subset of columns which will be displayed.
    <br><br>
    The possible values represents the corresponding column.
    The column "row" represents the row number within the current list.
    <br><br>
    Possible values: a combination of <code>row,state,timestamp,image,name,number,internal,external,connection,duration</code><br>
    Default Value is "row,state,timestamp,name,number,internal,external,connection,duration" (show all columns, except "image" as it needs to be configured first)<br><br>
  </ul>
  <br>
  <a name="FB_CALLLIST_events"></a>
  <b>Generated Events:</b><br><br>
  <ul>
    This module generates only readings if the attribute <a href="#FB_CALLLIST_create-readings">create-readings</a> is activated.
    The number and names of the readings depends on the selected columns (see attribute <a href="#FB_CALLLIST_visible-columns">visible-columns</a>) and the configured number of calls (see attribute <a href="#FB_CALLLIST_number-of-calls">number-of-calls</a>).
    <br><br>
    In general the following readings are always created if attribute <a href="#FB_CALLLIST_create-readings">create-readings</a> is activated:
    <br><br>
    <ul>
    <li><b>count-all</b> - The overall number of displayed calls.</li>
    <li><b>count-incoming</b> - The number of all displayed <i>incoming</i> calls</li>
    <li><b>count-outgoing</b> - The number of all displayed <i>outgoing</i> calls</li>
    <li><b>count-active</b> - The number of running (not yet completed) calls</li>
    <li><b>count-completed</b> - The number of already completed calls.</li>
    <li><b>count-missed-calls</b> - The number of missed calls (incoming).</li>
    </ul>
  </ul>
</ul>
=end html
=begin html_DE

<a name="FB_CALLLIST"></a>
<h3>FB_CALLLIST</h3>
<ul>
  Das FB_CALLLIST Modul erstellt eine Anrufliste f&uuml;r eine konfigurierte <a href="#FB_CALLMONITOR">FB_CALLMONITOR</a> Definition.
  Es speichert alle Anrufe und zeigt sie in einer historischen Tabelle an.
  <br><br>
  Es wird eine bereits konfigurierte FB_CALLMONITOR Definition ben&ouml;tigt, von der FB_CALLLIST die Events entsprechend verarbeiten kann.<br><br>
  Abh&auml;ngig von der Konfiguration der Attribute wird der Status als Icon oder als Textzeichen ausgegeben.
  Um die Icons korrekt anzeigen zu k&ouml;nnen, muss das openautomation Icon-Set in der entsprechenden FHEMWEB-Instanz konfiguriert sein (siehe dazu FHEMWEB Attribut <a href="#iconPath">iconPath</a>).
  <br><br>
  Die Icons haben verschiedene Farben:<br><br>
  <ul>
  <li><font color="blue"><b>blau</b></font> - Eingehender Anruf (aktiv oder beendet)</li>
  <li><font color="green"><b>gr&uuml;n</b></font> - Ausgehender Anruf (aktiv oder beendet))</li>
  <li><font color="red"><b>rot</b></font> - Verpasster Anruf (eingehend)</li>
  </ul>
  <br>
  Falls keine Icons verwendet werden sollen (siehe Attribut <a href="#FB_CALLLIST_show-icons">show-icons</a>), wird der Status wie folgt angezeigt:<br><br>
  <ul>

    <li><code><b>&lt;= ((o))</b></code></td><td> - Ausgehender Anruf (klingelt)</li>
    <li><code><b>=&gt; ((o))</b></code></td><td> - Eingehender Anruf (klingelt)</li>
    <br>
    <li><code><b>&lt;= [=]</b></code></td><td> - Ausgehender Anruf (laufendes Gespr&auml;ch)</li>
    <li><code><b>=&gt; [=]</b></code></td><td> - Eingehender Anruf (laufendes Gespr&auml;ch)</li>
    <br>
    <li><code><b>&lt;= X</b></code></td><td> - Ausgehender, erfolgloser Anruf (Gegenseite nicht abgenommen)</li>
    <li><code><b>=&gt; X</b></code></td><td> - Eingehender, erfolgloser Anruf (Verpasster Anruf)</li>
    <br>
    <li><code><b>=&gt; O_O</b></code></td><td> - Eingehender Anruf, der durch einen Anrufbeantworter entgegen genommen wurde</li>
    <br>
    <li><code><b>&lt;=</b></code></td><td> - Ausgehender Anruf (beendet)</li>
    <li><code><b>=&gt;</b></code></td><td> - Eingehender Anruf (beendet)</li>
  </ul>
  <br>
  <a name="FB_CALLLIST_define"></a>
  <b>Definition</b>
  <ul>
    <code>define &lt;Name&gt; FB_CALLLIST &lt;FB_CALLMONITOR Name&gt;</code><br>
  </ul>
  <br>
  <a name="FB_CALLLIST_set"></a>
  <b>Set-Kommandos</b><br>
  <ul>
  <li><b>clear</b> - l&ouml;scht die gesamte Anrufliste</li>
  <li><b>removeItem &lt;index&gt;</b> - l&ouml;scht eine spezifische Zeile aus der Anrufliste (Zeilennummer)</li>
  </ul>
  <br>

  <a name="FB_CALLLIST_get"></a>
  <b>Get</b><br>
  <ul>
  N/A
  </ul>
  <br>

  <a name="FB_CALLLIST_attr"></a>
  <b>Attributes</b><br><br>
  <ul>
    <li><a href="#do_not_notify">do_not_notify</a></li>
    <li><a href="#readingFnAttributes">readingFnAttributes</a></li>

    <br>

    <li><a name="FB_CALLLIST_answMachine-is-missed-call">answMachine-is-missed-call</a> 0,1</li>
    Sofern aktiviert, werden Anrufe, welche durch einen internen Anrufbeantworter beantwortet werden, als "verpasster Anruf" gewertet. Diese Funktionalit&auml;t ist nur relevant, wenn <a href="#FB_CALLLIST_list-type">list-type</a> auf "missed-call" gesetzt oder <a href="#FB_CALLLIST_create-readings">create-readings</a> aktiviert ist.
    <br><br>
    M&ouml;gliche Werte: 0 =&gt; deaktiviert, 1 =&gt; aktiviert (Anrufbeantworter gilt als "verpasster Anruf").<br>
    Standardwert ist 0 (deaktiviert)<br><br>

    <li><a name="FB_CALLLIST_create-readings">create-readings</a> 0,1</li>
    Sofern aktiviert, werden f&uuml;r alle sichtbaren Anrufe in der Liste entsprechende Readings und Events erzeugt.
    Es wird empfohlen das Attribut <a href="#event-on-change-reading">event-on-change-reading</a> auf den Wert <code>.*</code> zu stellen um die hohe Anzahl an Events in bestimmten F&auml;llen zu minimieren.<br><br>
    M&ouml;gliche Werte: 0 =&gt; keine Readings erstellen, 1 =&gt; Readings und Events werden erzeugt.<br>
    Standardwert ist 0 (keine Readings erstellen)<br><br>

    <li><a name="FB_CALLLIST_connection-mapping">connection-mapping</a> &lt;hash&gt;</li>
    Definiert eine eigene Zuordnung der Endger&auml;te (Reading: internal_connection) zu eigenen Bezeichnungen. Die Zuordnung erfolgt &uuml;ber eine Hash-Struktur.<br><br>
    z.B.<br>
    <ul>
    <code>attr &lt;name&gt; connection-mapping {'DECT_1' =&gt; 'Mobilteil K&uuml;che', 'FON1' =&gt; 'Fax', 'Answering_Machine_1' =&gt; 'Anrufbeantworter'}</code>
    </ul><br>
    Die jeweils zugeordnete Bezeichnung wird in der Anrufliste dann entsprechend angezeigt anstatt des originalen Werten von FB_CALLMONITOR.
    <br><br>
    Standardwert ist  <i>nicht gesetzt</i> (Keine Zuordnung, es werden die Originalwerte verwendet)
    <br><br>

    <li><a name="FB_CALLLIST_contactImageDirectory">contactImageDirectory</a> &lt;Verzeichnis&gt;</li>
    Sofern gesetzt, nutzt FB_CALLLIST dieses Verzeichnis um Kontaktbilder f&uuml;r jeden Anruf anzuzeigen.
    Diese Bilder werden in der Spalte "image" dargestellt, welche dazu explizit in dem Attribut <a href="#FB_CALLLIST_visible-columns">visible-columns</a> konfiguriert sein muss.
    Wenn in diesem Verzeichnis eine Bilddatei mit der externen Nummer als Dateiname (z.B. <code>0123456789.jpg</code> oder <code>0345678901.gif</code>) enthalten ist, wird diese als Kontaktbild in der Anrufliste verwendet.
    <br><br>
    Unterst&uuml;tzte Dateiformate: JPEG, GIF, PNG, BMP
    <br><br>
    Standardm&auml;&szlig;ig ist kein Verzeichnis vorkonfiguriert. Daher werden standardm&auml;&szlig;ig keine Kontaktbilder angezeigt.
    <br><br>
    
    <li><a name="FB_CALLLIST_contactImageDirectory">contactDefaultImage</a> &lt;Dateiname&gt;</li>
    Sofern Kontaktbilder verwendet werden (via Attribut <a href="#FB_CALLLIST_contactImageDirectory">contactImageDirectory</a>) und kein zugeh&ouml;riges Kontaktbild existiert oder die externe Rufnummer unbekannt ist,
    wird die konfigurierte Datei (z.B. <code>unknown.jpg</code>) als Kontaktbild verwendet.
    Die Datei muss sich dabei in dem Verzeichnis befinden, welches via Attribut <a href="#FB_CALLLIST_contactImageDirectory">contactImageDirectory</a> konfiguriert ist. 
    <br><br>
    Wenn nicht konfiguriert, werden keine Kontaktbilder in solchen F&auml;llen angezeigt.
    <br><br>
    
    <li><a name="FB_CALLLIST_disable">disable</a> 0,1,2,3</li>
    Optionales Attribut zur Deaktivierung der Anrufliste. Sofern aktiviert, werden keine Anruf-Events mehr verarbeitet und die Liste nicht weiter aktualisiert. Je nach gesetztem Wert verh&auml;lt sich FB_CALLLIST unterschiedlich.
    <br><br>
    M&ouml;gliche Werte:<ul>
      <li>0 =&gt; Anrufliste ist aktiv, verarbeitet Events und aktualisiert die Darstellung kontinuierlich.</li>
      <li>1 =&gt; Events werden NICHT verarbeitet. Die Darstellung wird NICHT aktualisiert (bleibt wie sie ist).</li>
      <li>2 =&gt; Events werden NICHT verarbeitet. Die Darstellung zeigt nur "disabled" an (keine Eintr&auml;ge mehr).</li>
      <li>3 =&gt; Events werden NICHT verarbeitet. Die Liste wird NICHT mehr angezeigt.</li>
      </ul><br>
    Standardwert ist 0 (aktiv)<br><br>

    <li><a name="FB_CALLLIST_disabledForIntervals">disabledForIntervals</a> HH:MM-HH:MM HH:MM-HH:MM...</li>
    Optionales Attribut zur Deaktivierung der Anrufliste innerhalb von bestimmten Zeitintervallen.
    Das Argument ist eine Leerzeichen-getrennte Liste von Minuszeichen-getrennten HH:MM Paaren (Stunde : Minute).
    Falls die aktuelle Uhrzeit zwischen diese Werte f&auml;llt, dann wird die Ausf&uuml;hrung, wie bei <a href="#FB_CALLLIST_disable">disable</a> gleich 1, ausgesetzt.
    Statt HH:MM kann man auch HH oder HH:MM:SS angeben.<br><br>
    Um einen Intervall um Mitternacht zu spezifizieren, muss man zwei einzelne Intervalle angeben, z.Bsp.:
    <pre>23:00-24:00 00:00-01:00</pre>
    Standardwert ist <i>nicht gesetzt</i> (dauerhaft aktiv)<br><br>

    <li><a name="FB_CALLLIST_processEventsWhileDisabled">processEventsWhileDisabled</a> 0,1</li>
    Sofern gesetzt, werden Events weiterhin verarbeitet, selbst wenn FB_CALLLIST deaktiviert ist (siehe <a href="FB_CALLLIST_disable">disabled</a> und <a href="FB_CALLLIST_disabledForIntervals">disabledForIntervals</a>).
    Sobald FB_CALLLIST wieder aktiviert wurde, stehen s&auml;mtliche Anrufe, w&auml;hrend FB_CALLLIST deaktiviert war, zur Verf&uuml;gung.
    <br><br>
    M&ouml;gliche Werte: 0 =&gt; keine Eventverabeitung wenn FB_CALLLIST deaktiviert ist, 1 =&gt; Events werden trotz deaktiviert FB_CALLLIST intern weiterhin verarbeitet.<br>
    Standardwert ist 0 (keine Eventverabeitung wenn deaktiviert)<br><br>

    <li><a name="FB_CALLLIST_expire-calls-after">expire-calls-after</a> &lt;Zeitfenster&gt;</li>
    Optionales Attribut um beendete Anrufe nach einem angegeben Zeitfenster automatisch aus der Anrufliste zu l&ouml;schen.
    Sobald ein beendetes Gespr&auml;ch &auml;lter ist als das angegebene Zeitfenster, wird es automatisch aus der Liste entfernt.
    <br><br>Ein Zeitfenster kann wie folgt angegeben werden:
    <ul>
    <li>als Minuten: <code>1 minute</code> oder <code>30 minutes</code></li>
    <li>als Stunden: <code>1 hour</code> oder <code>12 hours</code></li>
    <li>als Tage: <code>1 day</code> oder <code>5 days</code></li>
    <li>als Monate: <code>1 month</code> oder <code>6 months</code> (ein Monat entspricht hierbei 30 Tagen month is here equal to 30 days)</li>
    <li>als Jahr: <code>1 year</code> oder <code>2 years</code> (ein Jahr entspricht hierbei 365 Tagen)</li>
    </ul>
    <br>
    <b>WICHTIG:</b> Es wird hierbei der Endezeitpunkt eines Gespr&auml;chs betrachtet, nicht der Beginn des Gespr&auml;chs.<br><br>

    Wenn keine Einheit angegeben ist, wird die angegebene Zahl als Sekunden interpretiert. Es k&ouml;nnen auch Fliesskommazahlen mit einem Punkt als Kommastelle angegeben werden (z.B. <code>0.5 day</code>).
    Der Wert <code>0</code> bedeutet, das keine Gespr&auml;che nach einem gewissen Zeitfenster gel&ouml;scht werden.<br><br>
    Standardwert ist 0 (keine Gespr&auml;che werden nach einem Zeitfenster gel&ouml;scht)<br><br>

    <li><a name="FB_CALLLIST_external-mapping">external-mapping</a> &lt;Hash&gt;</li>
    Definiert eine eigene Zuordnung der externen Anschlussbezeichnung (Reading: external_connection) zu eigenen Bezeichnungen. Die Zuordnung erfolgt &uuml;ber eine Hash-Struktur.<br><br>
    z.B.<br>
    <ul>
    <code>attr &lt;name&gt; external-mapping {'ISDN' =&gt; 'Festnetz', 'SIP0' =&gt; 'Anbieter A', 'SIP1' =&gt; 'Anbieter B'}</code>
    </ul><br>
    Die jeweils zugeordnete Bezeichnung wird in der Anrufliste dann entsprechend angezeigt anstatt des originalen Werten von FB_CALLMONITOR.
    <br><br>
    Standardwert ist  <i>nicht gesetzt</i> (Keine Zuordnung, es werden die Originalwerte verwendet)
    <br><br>

    <li><a name="FB_CALLLIST_icon-mapping">icon-mapping</a> &lt;hash&gt;</li>
    Definiert eine eigene Zuordnung eines Anrufstatus zu einem Icon. Die Zuordnung erfolgt &uuml;ber eine Hash-Struktur.<br><br>
    z.B.<br>
    <ul>
    <code>attr &lt;name&gt; icon-mapping {'incoming.connected' =&gt; 'phone_ring_in@yellow', 'outgoing.missed' =&gt; 'phone_missed_out@red'}</code>
    </ul><br>
    Das entsprechende Icon wird an Stelle des Original-Icons bzw. Text verwendet. Sofern SVG-basierte Icons verwendet werden, kann man die Farbe optional definieren durch das Anf&uuml;gen via @ mit Name oder einem HTML Farbcode.
    <br><br>
    M&ouml;gliche Werte und ihre Standard-Icons sind:<br><br>
    <ul>
    <li><b>incoming.ring</b> =&gt; phone_ring@blue</li>
    <li><b>outgoing.ring</b> =&gt; phone_ring@green</li>
    <li><b>incoming.connected</b> =&gt; phone_ring_in@blue</li>
    <li><b>outgoing.connected</b> =&gt; phone_ring_in@green</li>
    <li><b>incoming.missed</b> =&gt; phone_missed_in@red</li>
    <li><b>outgoing.missed</b> =&gt; phone_missed_out@green</li>
    <li><b>incoming.done</b> =&gt; phone_call_end_in@blue</li>
    <li><b>outgoing.done</b> =&gt; phone_call_end_out@green</li>
    <li><b>incoming.tam</b> =&gt; phone_answering@blue</li>
    </ul>
    <br><br>
    Standardwert ist <i>nicht gesetzt</i> (Keine Zuordnung, es werden die Standard-Icons verwendet, sofern Icons akitivert sind)
    <br><br>

    <li><a name="FB_CALLLIST_internal-number-filter">internal-number-filter</a> &lt;hash&gt;</li>
    Dieses Attribut erm&ouml;glicht das Filtern der angezeigten Anrufe auf bestimmte interne Rufnummern sowie das Zuordnen von Namen zu den internen Rufnummern.<br><br>
    Es ist m&ouml;glich eine kommaseparierte Liste an internen Rufnummern anzugeben oder eine Hash-Tabelle in der man den internen Rufnummern eine eigene Bezeichnung zuweist.
    <br>
    <br>
    z.B.<br>
    <ul>
      <code>attr &lt;name&gt; internal-number-filter 304050,304060<br><br>
      attr &lt;name&gt;  internal-number-filter {'304050' =&gt; 'geschftl.', '304060' =&gt; 'privat'}</code>
    </ul>
    <br>
    <b>Wichtig:</b> Je nach Telefonanbieter kann der Wert die Ortsvorwahl enthalten. Die Rufnummer muss genauso angegeben werden, wie sie ohne eine Zuordnung in der Anrufliste auftaucht.<br><br>
    Wenn dieses Attribut gesetzt ist, werden nur die eingestellten Rufnummern in der Liste angezeigt.
    <br><br>
    Standardwert ist <i>nicht gesetzt</i> (alle internen Rufnummern werden angezeigt)
    <br><br>

    <li><a name="FB_CALLLIST_list-order">list-order</a> descending,ascending</li>
    Gibt an ob der neueste Anruf in der ersten Zeile (aufsteigend =&gt; descending) oder in der letzten Zeile (absteigend =&gt; ascending) in der Liste angezeigt werden soll. Dementsprechend rollt die Liste dann nach oben oder unten durch.<br><br>
    Standardwert ist "descending" (absteigend, neuester Anruf in der ersten Zeile)<br><br>

    <li><a name="FB_CALLLIST_list-type">list-type</a> all,incoming,outgoing,missed-calls,completed,active</li>
    Ist dieses Attribut gesetzt, werden nur bestimmte Typen von Anrufen in der Liste angezeigt:<br><br>
    <ul>
    <li><code>all</code> - Alle Anrufe werden angezeigt</li>
    <li><code>incoming</code> - Alle eingehenden Anrufe werden angezeigt (aktive und abgeschlossene)</li>
    <li><code>outgoing</code> - Alle ausgehenden Anrufe werden angezeigt (aktive und abgeschlossene)</li>
    <li><code>missed-calls</code> - Alle eingehenden, verpassten Anrufe werden angezeigt.</li>
    <li><code>completed</code> - Alle abgeschlossenen Anrufe werden angezeigt (eingehend und ausgehend)</li>
    <li><code>active</code> - Alle aktuell laufenden Anrufe werden angezeigt (eingehend und ausgehend)</li>
    </ul><br>
    Standardwert ist "all" (alle Anrufe anzeigen)<br><br>

    <li><a name="FB_CALLLIST_no-heading">no-heading</a> 0,1</li>
    Sofern aktiviert, wird die &Uuml;berschriftenzeile ausserhalb der Liste inkl. Link auf die Detail-Seite der aktuellen Definition ausgeblendet.<br><br>
    M&ouml;gliche Werte: 0 =&gt; &Uuml;berschriftenzeile wird angezeigt , 1 =&gt; &Uuml;berschriftenzeile wird ausgeblendet<br>
    Standardwert ist 1 (&Uuml;berschriftenzeile wird angezeigt)<br><br>

    <li><a name="FB_CALLLIST_no-table-header">no-table-header</a> 0,1</li>
    Sofern aktiviert, wird die Kopfzeile der Tabelle f&uuml;r die aktuelle Definition ausgeblendet.<br><br>
    M&ouml;gliche Werte: 0 =&gt; Kopfzeile wird angezeigt , 1 =&gt; Kopfzeile wird ausgeblendet<br>
    Standardwert ist 1 (Kopfzeile wird angezeigt)<br><br>

    <li><a name="FB_CALLLIST_number-cmd">number-cmd</a> &lt;Befehl&gt;</li>
    Kann gesetzt werden, um ein FHEM-Befehl oder Perl-Code (in geschweiften Klammern: { ... } ) auszuf&uuml;hren, wenn man auf eine Rufnummer in der Anrufliste klickt.
    Der Platzhalter <code>$NUMBER</code> wird dabei mit der entsprechenden Rufnummer der jeweiligen Zeile ersetzt.
    <br><br>
    Damit kann man beispielsweise einen R&uuml;ckruf starten.
    e.g.:<br><br>
    <ul>
    <li><code>set FRITZBOX call $NUMBER</code></li>
    <li><code>{dialNumber("$NUMBER")}</code></li>
    </ul>
    <br>
    Sofern nicht gesetzt, wird kein Link angezeigt.<br><br>

    <li><a name="FB_CALLLIST_number-of-calls">number-of-calls</a> 1..40</li>
    Setzt die maximale Anzahl an Eintr&auml;gen in der Anrufliste. Sollte die Anrufliste voll sein, wird das &auml;lteste Gespr&auml;ch gel&ouml;scht.<br><br>
    Standardwert sind 5 Eintr&auml;ge<br><br>

    <li><a name="FB_CALLLIST_show-icons">show-icons</a> 0,1</li>
    Im Normalfall wird der Status eines jeden Anrufs mit einem Icon angezeigt. Dazu muss das openautomation Icon-Set im iconpath-Attribut der entsprechenden FHEMWEB Instanz konfiguriert sein. Sollte man keine Icons w&uuml;nschen, so kann man diese hiermit abschalten. Der Status wird dann mittels Textzeichen dargestellt.<br><br>
    M&ouml;gliche Werte: 0 =&gt; keine Icons , 1 =&gt; benutze Icons<br>
    Standardwert ist 1 (benutze Icons)<br><br>

    <li><a name="FB_CALLLIST_time-format-string">time-format-string</a> &lt;String&gt;</li>
    Definiert einen Formatierungs-String welcher benutzt wird um die Zeitangaben in der Anrufliste nach eigenen W&uuml;nschen anzupassen. Es stehen hier eine ganze Reihe an Platzhaltern zur Verf&uuml;gung um die einzelnen Elemente einer Datums-/Zeitangabe einzeln zu setzen. Die m&ouml;glichen Werte sind alle Standard POSIX strftime() Platzhalter. G&auml;ngige Platzhalter sind:<br><br>
    <ul>
    <li><code>%a</code> - Der abgek&uuml;rzte Wochentagname</li>
    <li><code>%b</code> - Der abgek&uuml;rzte Monatsname</li>
    <li><code>%S</code> - Die Sekunden als Dezimalzahl</li>
    <li><code>%M</code> - Die Minuten als Dezimalzahl</li>
    <li><code>%H</code> - Die Stunden als Dezimalzahl</li>
    <li><code>%d</code> - Der Tag im Monat als Dezimalzahl</li>
    <li><code>%m</code> - Der Monat als Dezimalzahl</li>
    <li><code>%Y</code> - Das Jahr als Dezimalzahl (4-stellig).</li>
    </ul><br>
    Es gibt hierf&uuml;r noch weitere Platzhalter. Weitere Informationen dazu findet man in der Manpage von <code>strftime()</code> oder der Dokumentation des entsprechenden Perl Interpreters.
    <br><br>
    Standardwert ist "%a, %d %b %Y %H:%M:%S" (entspricht "So, 07 Jun 2015 12:50:09")<br><br>
    <li><a name="FB_CALLLIST_language">language</a> en,de</li>
    Definiert die Sprache in der die Anrufliste angezeigt werden soll (Tabellenkopf, Datum). Die entsprechende Sprache muss auch im Betriebssystem installiert und unterst&uuml;tzt werden.<br><br>
    M&ouml;gliche Werte: en =&gt; Englisch , de =&gt; Deutsch<br>
    Standardwert ist en (Englisch)<br><br>

    <li><a name="FB_CALLLIST_visible-columns">visible-columns</a> row,state,timestamp,image,name,number,internal,external,connection,duration</li>
    Legt fest, welche Spalten in welcher Reihenfolge (von links nach rechts) in der Anrufliste angezeigt werden sollen.
    Es m&uuml;ssen nicht alle verf&uuml;gbaren Spalten angezeigt werden.
    Es kann auch eine Auswahl von einzelnen Spalten angezeigt werden.
    <br><br>
    Die m&ouml;glichen Werte repr&auml;sentieren die jeweilige Spalte.
    Der Wert "row" steht f&uuml;r die Zeilennummer innerhalb der Liste.
    <br><br>
    M&ouml;gliche Werte: Eine Kombination der folgenden Werte in der gew&uuml;nschten Reihenfolge: <code>row,state,timestamp,image,name,number,internal,external,connection,duration</code><br>
    Standardwert ist "row,state,timestamp,name,number,internal,external,connection,duration" (Anzeige aller Spalten bis auf "image", da diese erst konfiguriert werden muss)<br><br>
  </ul>
  <br>
  <a name="FB_CALLLIST_events"></a>
  <b>Generierte Events:</b><br><br>
  <ul>
    Dieses Modul generiert Readings/Events, sofern das Attribut <a href="#FB_CALLLIST_create-readings">create-readings</a> aktiviert ist.
    Die Anzahl, sowie der Name der Readings ist von den gew&auml;hlten Spalten (Attribut: <a href="#FB_CALLLIST_visible-columns">visible-columns</a>), sowie der Anzahl der anzuzeigenden Anrufe abh&auml;ngig (Attribut: <a href="#FB_CALLLIST_number-of-calls">number-of-calls</a>).
    <br><br>
    Generell werden folgende Readings/Events immer erzeugt, sofern das Attribut <a href="#FB_CALLLIST_create-readings">create-readings</a> aktiviert ist:
    <br><br>
    <ul>
    <li><b>count-all</b> - Die Gesamtanzahl aller angezeigten Anrufe</li>
    <li><b>count-incoming</b> - Die Anzahl aller angezeigten <i>eingehenden</i> Anrufe</li>
    <li><b>count-outgoing</b> - Die Anzahl aller angezeigten <i>ausgehenden</i> Anrufe</li>
    <li><b>count-active</b> - Die Anzahl aller laufenden (noch nicht beendeten) Anrufe</li>
    <li><b>count-completed</b> - Die Anzahl aller bereits abgeschlossenen Anrufe</li>
    <li><b>count-missed-calls</b> - Die Anzahl aller verpassten Anrufe (eingehend)</li>
    </ul>
  </ul>
</ul>
=end html_DE
=cut
