package FHEM::EaseeWallbox;

# use GPUtils qw(GP_Import GP_Export); hast Du auch weiter unten stehen

use strict;
use warnings;
use Data::Dumper;
use utf8;
use Encode qw( encode_utf8 );
use HttpUtils;
use JSON;
use DateTime;
use DateTime::Format::Strptime;

# try to use JSON::MaybeXS wrapper
#   for chance of better performance + open code
eval {
    require JSON::MaybeXS;
    import JSON::MaybeXS qw( decode_json encode_json );
    1;
} or do {

    # try to use JSON wrapper
    #   for chance of better performance
    eval {
        # JSON preference order
        local $ENV{PERL_JSON_BACKEND} =
          'Cpanel::JSON::XS,JSON::XS,JSON::PP,JSON::backportPP'
          unless ( defined( $ENV{PERL_JSON_BACKEND} ) );

        require JSON;
        import JSON qw( decode_json encode_json );
        1;
    } or do {

        # In rare cases, Cpanel::JSON::XS may
        #   be installed but JSON|JSON::MaybeXS not ...
        eval {
            require Cpanel::JSON::XS;
            import Cpanel::JSON::XS qw(decode_json encode_json);
            1;
        } or do {

            # In rare cases, JSON::XS may
            #   be installed but JSON not ...
            eval {
                require JSON::XS;
                import JSON::XS qw(decode_json encode_json);
                1;
            } or do {

                # Fallback to built-in JSON which SHOULD
                #   be available since 5.014 ...
                eval {
                    require JSON::PP;
                    import JSON::PP qw(decode_json encode_json);
                    1;
                } or do {

                    # Fallback to JSON::backportPP in really rare cases
                    require JSON::backportPP;
                    import JSON::backportPP qw(decode_json encode_json);
                    1;
                };
            };
        };
    };
};

# Import von Funktionen und/oder Variablen aus der FHEM main
# man kann ::Funktionaname wählen und sich so den Import schenken. Variablen sollten aber
#   sauber importiert werden
# use GPUtils qw(GP_Import);
use GPUtils qw(GP_Import GP_Export)
  ;    # da Du beide Funktionen aus dem package verwendest

## Import der FHEM Funktionen
#-- Run before package compilation
BEGIN {
    # Import from main context
    GP_Import(
        qw(
          readingFnAttributes
          Log3
          readingsBeginUpdate
          readingsEndUpdate
          readingsBulkUpdate
          readingsSingleUpdate
          readingsDelete
          InternalVal
          ReadingsVal
          RemoveInternalTimer
          InternalTimer
          HttpUtils_NonblockingGet
          HttpUtils_BlockingGet
          gettimeofday
          getUniqueId
          Attr
          )
    );
}

#-- Export to main context with different name
GP_Export(
    qw(
      Initialize
      )
);

my %gets = (
    update  => "noArg",
    health  => "noArg",
    charger => "noArg",
);

my %sets = (
    enabled                  => "",
    disabled                 => "",
    enableSmartButton        => "true,false",
    authorizationRequired    => "true,false",
    startCharging            => "",
    stopCharging             => "",
    pauseCharging            => "",
    resumeCharging           => "",
    toggleCharging           => "",
    interval                 => "",
    refreshToken             => "noArg",
    cableLock                => "true,false",
    reboot                   => "noArg",
    updateFirmware           => "noArg",
    enableSmartCharging      => "true,false",
    ledStripBrightness       => "",
    overrideChargingSchedule => "",
    pairRfidTag              => "",
    pricePerKWH              => "",
    activateTimer            => "",
    deactivateTimer          => "",
);

## Datapoint, all behind API URI
my %dpoints = (
    getOAuthToken             => 'accounts/login',
    getRefreshToken           => 'accounts/refresh_token',
    getProfile                => 'accounts/profile',
    getChargingSession        => 'chargers/#ChargerID#/sessions/ongoing',
    getChargers               => 'accounts/chargers',
    getProducts               => 'accounts/products?userId=#UserId#',
    getChargerSite            => 'chargers/#ChargerID#/site',
    getChargerDetails         => 'chargers/#ChargerID#/details',
    getChargerConfiguration   => 'chargers/#ChargerID#/config',
    getChargerSessionsMonthly => 'sessions/charger/#ChargerID#/monthly',
    getChargerSessionsDaily   => 'sessions/charger/#ChargerID#/daily',
    getChargerState           => 'chargers/#ChargerID#/state',
    getCurrentSession         => 'chargers/#ChargerID#/sessions/ongoing',
    setCableLockState         => 'chargers/#ChargerID#/commands/lock_state',
    setReboot                 => 'chargers/#ChargerID#/commands/reboot',
    setUpdateFirmware      => 'chargers/#ChargerID#/commands/update_firmware',
    setEnableSmartCharging => 'chargers/#ChargerID#/commands/smart_charging',
    setStartCharging       => 'chargers/#ChargerID#/commands/start_charging',
    setStopCharging        => 'chargers/#ChargerID#/commands/stop_charging',
    setPauseCharging       => 'chargers/#ChargerID#/commands/pause_charging',
    setResumeCharging      => 'chargers/#ChargerID#/commands/resume_charging',
    setToggleCharging      => 'chargers/#ChargerID#/commands/toggle_charging',
    setOverrideChargingSchedule =>
      'chargers/#ChargerID#/commands/override_schedule',
    setPairRFIDTag =>
      'chargers/#ChargerID#/commands/set_rfid_pairing_mode_async',
    changeChargerSettings => 'chargers/#ChargerID#/settings',
    setChargingPrice      => 'sites/#SiteID#/price',
);
my %reasonsForNoCurrent = (
    0 => 'OK',                               #charger is allocated current
    1 => 'MaxCircuitCurrentTooLow',
    2 => 'MaxDynamicCircuitCurrentTooLow',
    3 => 'MaxDynamicOfflineFallbackCircuitCurrentTooLow',
    4 => 'CircuitFuseTooLow',
    5 => 'WaitingInQueue',
    6 => 'WaitingInFully'
    , #charged queue (charger assumes one of: EV uses delayed charging, EV charging complete)
    7 => 'IllegalGridType',
    8 => 'PrimaryUnitHasNotReceivedCurrentRequestFromSecondaryUnit',
    50  => 'SecondaryUnitNotRequestingCurrent',    #no car connected...
    51  => 'MaxChargerCurrentTooLow',
    52  => 'MaxDynamicChargerCurrentTooLow',
    53  => 'ChargerDisabled',
    54  => 'PendingScheduledCharging',
    55  => 'PendingAuthorization',
    56  => 'ChargerInErrorState',
    100 => 'Undefined'
);
my %phaseModes = (
    1 => 'Locked to single phase',
    2 => 'Auto',
    3 => 'Locked to three phase',
);
my %operationModes = (
    0 => "Disconnected",
    1 => "Standby",
    2 => "Paused",
    3 => 'Charging',
    4 => 'Completed',
    5 => 'Error',
    6 => 'CarConnected'
);

my %commandCodes = (
    1  => "Reboot",
    2  => "Poll single observation",
    3  => "Poll all observations",
    4  => "Upgrade Firmware",
    5  => "Download settings",
    7  => "Scan Wifi",
    11 => "Set smart charging",
    23 => "Abort charging",
    25 => "Start Charging",
    26 => "Stop Charging",
    29 => "Set enabled",
    30 => "Set cable lock",
    11 => "Set smart charging",
    40 => "Set lightstripe brightness",
    43 => "Add keys",
    44 => "Clear keys",
    48 => "Pause/Resume/Toggle Charging",
    60 => "Add schedule",
    61 => "Cear Schedule",
    62 => "Get Schedule",
    63 => "Override Schedule",
    64 => "Purge Schedule",
    69 => "Set RFID Pairing Mode",
);

#Private function to evaluate command-lists
# private funktionen beginnen immer mit _

#############################
sub _GetCmdList {
    my ( $hash, $cmd, $commands ) = @_;

    my %cmdArray = %$commands;
    my $name     = $hash->{NAME};

    #return, if cmd is valid
    return if ( defined($cmd) and defined( $cmdArray{$cmd} ) );

    #response for gui or the user, if command is invalid
    my $retVal;
    foreach my $mySet ( keys %cmdArray ) {

        #append set-command
        $retVal = $retVal . " "
          if ( defined($retVal) )
          ; # Macht denke ich keinen Sinn da durch my $retVal bereits $retVal definiert ist
        $retVal = $retVal . $mySet;

        #get options
        my $myOpt = $cmdArray{$mySet};

        #append option, if valid
        $retVal = $retVal . ":" . $myOpt
          if ( defined($myOpt) and ( length($myOpt) > 0 ) );
        $myOpt = "" if ( !defined($myOpt) );

#Log3 ($name, 5, "parse cmd-table - Set:$mySet, Option:$myOpt, RetVal:$retVal");
    }
    if ( !defined($retVal) ) {
        return "error while parsing set-table";
    }

    return "Unknown argument $cmd, choose one of " . $retVal;

    # versuche wo wenig wie möglich if else zu verwenden.
    # else {
    #     $retVal = "Unknown argument $cmd, choose one of " . $retVal;
    # }
}

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

    $hash->{DefFn}   = \&Define;
    $hash->{UndefFn} = \&Undef;
    $hash->{SetFn}   = \&Set;
    $hash->{GetFn}   = \&Get;
    $hash->{AttrFn}  = \&Attr;
    $hash->{ReadFn}  = \&Read;
    $hash->{WriteFn} = \&Write;

    $hash->{AttrList} =
        'expertMode:yes,no '
      . 'ledStuff:yes,no '
      . 'SmartCharging:true,false '
      . $readingFnAttributes;

    #Log3, 'EaseeWallbox', 3, "EaseeWallbox module initialized.";
    return;
}

sub Define {
    my ( $hash, $def ) = @_;
    my @param = split( "[ \t]+", $def );
    my $name  = $hash->{NAME};

    # set API URI as Internal Key
    $hash->{APIURI} = 'https://api.easee.cloud/api/';

    Log3 $name, 3, "EaseeWallbox_Define $name: called ";

    my $errmsg = '';

# Check parameter(s) - Must be min 4 in total (counts strings not purly parameter, interval is optional)
    if ( int(@param) < 4 ) {
        $errmsg = return
"syntax error: define <name> EaseeWallbox <username> <password> [Interval]";
        Log3 $name, 1, "EaseeWallbox $name: " . $errmsg;
        return $errmsg;
    }

    #Check if the username is an email address
    if ( $param[2] =~ /^.+@.+$/x )
    { # Regular expression without "/x" flag. See page 236 of PBP (RegularExpressions::RequireExtendedFormatting)
        my $username = $param[2];
        $hash->{Username} = $username;
    }
    else {
        $errmsg =
"specify valid email address within the field username. Format: define <name> EaseeWallbox <username> <password> [interval]";
        Log3 $name, 1, "EaseeWallbox $name: " . $errmsg;
        return $errmsg;
    }

    #Take password and use custom encryption.
    # Encryption is taken from fitbit / withings module
    my $password = _encrypt( $param[3] );

    $hash->{Password} = $password;

    if ( defined $param[4] ) {
        $hash->{DEF} = sprintf( "%s %s %s",
            InternalVal( $name, 'Username', undef ),
            $password, $param[4] );
    }
    else {
        $hash->{DEF} = sprintf( "%s %s",
            InternalVal( $name, 'Username', undef ), $password );
    }

    #Check if interval is set and numeric.
    #If not set -> set to 60 seconds
    #If less then 5 seconds set to 5
    #If not an integer abort with failure.
    my $interval = 60;
    if ( defined $param[4] ) {
        if ( $param[4] =~ /^\d+$/x )
        { # Regular expression without "/x" flag. See page 236 of PBP (RegularExpressions::RequireExtendedFormatting)
            $interval = $param[4];
        }
        else {
            $errmsg =
"Specify valid integer value for interval. Whole numbers > 5 only. Format: define <name> EaseeWallbox <username> <password> [interval]";
            Log3 $name, 1, "EaseeWallbox $name: " . $errmsg;
            return $errmsg;
        }
    }

    if ( $interval < 5 ) { $interval = 5; }
    $hash->{INTERVAL} = $interval;

    readingsSingleUpdate( $hash, 'state', 'Undefined', 0 );

    #Initial load of data
    WriteToCloudAPI( $hash, 'getChargers', 'GET' );

    Log3 $name, 1,
      sprintf( "EaseeWallbox_Define %s: Starting timer with interval %s",
        $name, InternalVal( $name, 'INTERVAL', undef ) );
    InternalTimer( gettimeofday() + InternalVal( $name, 'INTERVAL', undef ),
        "FHEM::EaseeWallbox::UpdateDueToTimer", $hash )
      if ( defined $hash );

    ## return; sollte es nicht geben, ein return; ist per see mit Rückgabe undef
    return;
}

sub Undef {
    my ( $hash, $arg ) = @_;

    RemoveInternalTimer($hash);
    return;
}

sub Get {
    my ( $hash, $name, @args ) = @_;

    return '"get EaseeWallbox" needs at least one argument'
      if ( int(@args) < 1 );

    my $opt = shift @args;

    #create response, if cmd is wrong or gui asks
    my $cmdTemp = _GetCmdList( $hash, $opt, \%gets );
    return $cmdTemp if ( defined($cmdTemp) );

    $hash->{LOCAL} = 1;
    WriteToCloudAPI( $hash, 'getChargers', 'GET' ) if $opt eq "charger";
    RefreshData($hash)                             if $opt eq "update";
    delete $hash->{LOCAL};
    return;
}

sub Set {
    my ( $hash, $name, @param ) = @_;

    return '"set $name" needs at least one argument' if ( int(@param) < 1 );

    my $opt = shift @param;
    $value = join( "", @param );
    my %message;

    #create response, if cmd is wrong or gui asks
    my $cmdTemp = _GetCmdList( $hash, $opt, \%sets );
    return $cmdTemp if ( defined($cmdTemp) );

    if ( $opt eq "deactivateTimer" ) {

# Cascading if-elsif chain. See pages 117,118 of PBP (ControlStructures::ProhibitCascadingIfElse) kann man anders machen. Später machen wir das
        RemoveInternalTimer($hash);
        Log3 $name, 1,
"EaseeWallbox_Set $name: Stopped the timer to automatically update readings";
        readingsSingleUpdate( $hash, 'state', 'Initialized', 0 );
        return;
    }
    elsif ( $opt eq "activateTimer" ) {

        #Update once manually and then start the timer
        RemoveInternalTimer($hash);
        $hash->{LOCAL} = 1;
        RefreshData($hash);
        delete $hash->{LOCAL};
        InternalTimer( gettimeofday() + InternalVal( $name, 'INTERVAL', undef ),
            "FHEM::EaseeWallbox::UpdateDueToTimer", $hash );
        readingsSingleUpdate( $hash, 'state', 'Started', 0 );
        Log3 $name, 1,
          sprintf(
"EaseeWallbox_Set %s: Updated readings and started timer to automatically update readings with interval %s",
            $name, InternalVal( $name, 'INTERVAL', undef ) );
    }
    elsif ( $opt eq "interval" )
    { # interval wird immer über Attribut gesetzt. Also in die Funktion AttrFn aus Initialize
        my $interval = shift @param;

        $interval = 60 unless defined($interval);
        if ( $interval < 5 ) { $interval = 5; }

        Log3 $name, 1, "EaseeWallbox_Set $name: Set interval to" . $interval;
        $hash->{INTERVAL} = $interval;
    }
    elsif ( $opt eq "cableLock" ) {

        $message{'state'} = $value;
        WriteToCloudAPI( $hash, 'setCableLockState', 'POST', \%message );
    }
    elsif ( $opt eq "pricePerKWH" ) {

        $message{'currencyId'} = "EUR";
        $message{'vat'}        = "19";
        $message{'costPerKWh'} = shift @param;
        WriteToCloudAPI( $hash, 'setChargingPrice', 'POST', \%message );
    }
    elsif ( $opt eq "pairRfidTag" ) {
        my $timeout = shift @param;

      #if (defined $timeout and /^\d+$/)         { print "is a whole number\n" }
        $timeout = '60' if not defined $timeout or $timeout = '';

        $message{'timeout'} = "60";
        WriteToCloudAPI( $hash, 'setPairRFIDTag', 'POST', \%message );
    }
    elsif ( $opt eq "enableSmartCharging" ) {

        $message{'smartCharging'} = shift @param;
        WriteToCloudAPI( $hash, 'changeChargerSettings', 'POST', \%message );
    }
    elsif ( $opt eq "enabled" ) {

        $message{'enabled'} = "true";
        WriteToCloudAPI( $hash, 'changeChargerSettings', 'POST', \%message );
    }
    elsif ( $opt eq "disabled" ) {

        $message{'enabled'} = "false";
        WriteToCloudAPI( $hash, 'changeChargerSettings', 'POST', \%message );
    }
    elsif ( $opt eq "authorizationRequired" ) {

        $message{'authorizationRequired'} = shift @param;
        WriteToCloudAPI( $hash, 'changeChargerSettings', 'POST', \%message );
    }
    elsif ( $opt eq "enableSmartButton" ) {

        $message{'smartButtonEnabled'} = shift @param;
        WriteToCloudAPI( $hash, 'changeChargerSettings', 'POST', \%message );
    }
    elsif ( $opt eq "ledStripBrightness" ) {

        $message{'ledStripBrightness'} = shift @param;
        WriteToCloudAPI( $hash, 'changeChargerSettings', 'POST', \%message );
    }
    else {
        $hash->{LOCAL} = 1;
        WriteToCloudAPI( $hash, 'setStartCharging', 'POST' )
          if $opt eq "startCharging";
        WriteToCloudAPI( $hash, 'setStopCharging', 'POST' )
          if $opt eq 'stopCharging';
        WriteToCloudAPI( $hash, 'setPauseCharging', 'POST' )
          if $opt eq 'pauseCharging';
        WriteToCloudAPI( $hash, 'setResumeCharging', 'POST' )
          if $opt eq 'resumeCharging';
        WriteToCloudAPI( $hash, 'setToggleCharging', 'POST' )
          if $opt eq 'toggleCharging';
        WriteToCloudAPI( $hash, 'setUpdateFirmware', 'POST' )
          if $opt eq 'updateFirmware';
        WriteToCloudAPI( $hash, 'setOverrideChargingSchedule', 'POST' )
          if $opt eq 'overrideChargingSchedule';
        WriteToCloudAPI( $hash, 'setReboot', 'POST' ) if $opt eq 'reboot';
        _loadToken($hash)                             if $opt eq 'refreshToken';
        delete $hash->{LOCAL};
    }
    readingsSingleUpdate( $hash, 'state', 'Initialized', 0 )
      ; # Die Modulinstanz ist doch nicht erst bei einem set Initialized, das ist doch schon nach dem define. Wenn dann ist hier ein status ala "processing setter" oder so.
    return;
}

sub Attr {
    my ( $cmd, $name, $attrName, $attrVal ) = @_;

    # hier kannst Du das setzen des Intervals umsetzen
    return;
}

sub RefreshData {
    my $hash = shift;
    my $name = $hash->{NAME};
    WriteToCloudAPI( $hash, 'getChargerSite',            'GET' );
    WriteToCloudAPI( $hash, 'getChargerState',           'GET' );
    WriteToCloudAPI( $hash, 'getCurrentSession',         'GET' );
    WriteToCloudAPI( $hash, 'getChargerConfiguration',   'GET' );
    WriteToCloudAPI( $hash, 'getChargerSessionsMonthly', 'GET' );
    WriteToCloudAPI( $hash, 'getChargerSessionsDaily',   'GET' );

    return;    # immer mit einem return eine funktion beenden
}

sub UpdateDueToTimer {
    my ($hash) = @_;
    my $name = $hash->{NAME};

#local allows call of function without adding new timer.
#must be set before call ($hash->{LOCAL} = 1) and removed after (delete $hash->{LOCAL};)
    if ( !$hash->{LOCAL} ) {
        RemoveInternalTimer($hash);

        #Log3 "Test", 1, Dumper($hash);
        InternalTimer( gettimeofday() + InternalVal( $name, 'INTERVAL', undef ),
            "FHEM::EaseeWallbox::UpdateDueToTimer", $hash );
    }

    return RefreshData($hash);
}

sub WriteToCloudAPI {
    my $hash    = shift;
    my $dpoint  = shift;
    my $method  = shift;
    my $message = shift;
    my $name    = $hash->{NAME};
    my $url     = $hash->{APIURI} . $dpoints{$dpoint};

    #########
    # CHANGE THIS
    my $payload;
    $payload = encode_json \%$message if defined $message;
    my $deviceId = "WC1";

    if ( not defined $hash ) {
        my $msg =
          "Error on EaseeWallbox_WriteToCloudAPI. Missing hash variable";
        Log3 'EaseeWallbox', 1, $msg;
        return $msg;
    }

    #Check if chargerID is required in URL and replace or alert.
    if ( $url =~ m/#ChargerID#/x )
    { # Regular expression without "/x" flag. See page 236 of PBP (RegularExpressions::RequireExtendedFormatting)
        my $chargerId = ReadingsVal( $name, 'charger_id', undef );
        if ( not defined $chargerId ) {
            my $error =
"Error on EaseeWallbox_WriteToCloudAPI. Missing charger_id. Please ensure basic data is available.";
            Log3 'EaseeWallbox', 1, $error;
            return $error;
        }
        $url =~ s/#ChargerID#/$chargerId/xg
          ; # Regular expression without "/x" flag. See page 236 of PBP (RegularExpressions::RequireExtendedFormatting)
    }

    #Check if siteID is required in URL and replace or alert.
    if ( $url =~ m/#SiteID#/x )
    { # Regular expression without "/x" flag. See page 236 of PBP (RegularExpressions::RequireExtendedFormatting)
        my $siteId = ReadingsVal( $name, 'site_id', undef );
        if ( not defined $siteId ) {
            my $error =
"Error on EaseeWallbox_WriteToCloudAPI. Missing site_id. Please ensure basic data is available.";
            Log3 'EaseeWallbox', 1, $error;
            return $error;
        }
        $url =~ s/#SiteID#/$siteId/xg
          ; # Regular expression without "/x" flag. See page 236 of PBP (RegularExpressions::RequireExtendedFormatting)
    }

    my $CurrentTokenData = _loadToken($hash);
    my $header           = {
        "Content-Type" => "application/json;charset=UTF-8",
        "Authorization" =>
          "$CurrentTokenData->{'tokenType'} $CurrentTokenData->{'accessToken'}"
    };

    # $method ist GET oder POST
    # bei POST ist $payload gleich data

    HttpUtils_NonblockingGet(
        {
            url                => $url,
            timeout            => 15,
            incrementalTimeout => 1,
            hash               => $hash,
            dpoint             => $dpoint,
            device_id          => $deviceId,
            data               => $payload,
            method             => $method,
            header             => $header,
            callback           => \&ResponseHandling
        }
    );
    return;
}

sub ResponseHandling {
    my $param = shift;
    my $err   = shift;
    my $data  = shift;
    my $hash  = $param->{hash};
    my $name  = $hash->{NAME};

    Log3 $name, 4, "Callback received." . $param->{url};

    if ( $err ne "" )    # wenn ein Fehler bei der HTTP Abfrage aufgetreten ist
    {
        Log3 $name, 3,
            "error while requesting "
          . $param->{url}
          . " - $err";    # Eintrag fürs Log
        readingsSingleUpdate( $hash, "lastResponse", "ERROR $err", 1 );
        return;
    }

    my $code = $param->{code};
    if ( $code == 404 and $param->{dpoint} eq 'getCurrentSession' )
    {    # Entweder == dann number z.B. 404 oder wenn eq dann String also '404'
        readingsDelete( $hash, 'session_energy' );
        readingsDelete( $hash, 'session_start' );
        readingsDelete( $hash, 'session_end' );
        readingsDelete( $hash, 'session_chargeDurationInSeconds' );
        readingsDelete( $hash, 'session_firstEnergyTransfer' );
        readingsDelete( $hash, 'session_lastEnergyTransfer' );
        readingsDelete( $hash, 'session_pricePerKWH' );
        readingsDelete( $hash, 'session_chargingCost' );
        readingsDelete( $hash, 'session_id' );
        return;
    }

    if ( $code >= 400 ) {
        Log3 $name, 3,
            "HTTPS error while requesting "
          . $param->{url}
          . " - $code";    # Eintrag fürs Log
        readingsSingleUpdate( $hash, "lastResponse", "ERROR: HTTP Code $code",
            1 );
        return;
    }

    Log3 $name, 3,
      "Received non-blocking data from EaseeWallbox regarding current session ";

    Log3 $name, 4, "FHEM -> EaseeWallbox: " . $param->{url};
    Log3 $name, 4, "FHEM -> EaseeWallbox: " . $param->{message}
      if ( defined $param->{message} );
    Log3 $name, 4, "EaseeWallbox -> FHEM: " . $data;
    Log3 $name, 5, '$err: ' . $err;
    Log3 $name, 5, "method: " . $param->{method};
    Log3 $name, 2, "Something gone wrong"
      if ( $data =~ "/EaseeWallboxMode/" );

    my $decoded_json;

    eval { $decoded_json = decode_json($data) }; # statt eval ist es empfohlen catch try zu verwenden. Machen wir später
    if ($@) {
        Log3 $name, 3, "GardenaSmartBridge ($name) - JSON error while request";
    }

    Log3 $name, 5, 'Decoded: ' . Dumper($decoded_json);
    Log3 $name, 5, 'Ref of d: ' . ref($decoded_json);

    my $value;

    if (    defined $decoded_json
        and $decoded_json ne ''
        and ref($decoded_json) eq "HASH"
        or ( ref($decoded_json) eq "ARRAY" and $decoded_json > 0 ) )
    {
        if ( $param->{dpoint} eq 'getChargers' ) {
            Processing_DpointGetChargers( $hash, $decode_json );
            return;
        }

        if ( $param->{dpoint} eq 'getChargerSessionsDaily' ) {
            Processing_DpointGetChargerSessionsDaily( $hash, $decode_json );
            return;
        }

        # Und so weiter und so weiter mit den einzelnen Funktionen !!!

        if ( $param->{dpoint} eq 'getChargerSessionsMonthly' ) {
            Log3 $name, 5, 'Evaluating getChargerSessionsMonthly';
            my @x = $decoded_json;
            my @a = ( -6 .. -1 );
            readingsBeginUpdate($hash);
            for (@a) {
                Log3 $name, 5, 'laeuft noch: ' . $_;
                readingsBulkUpdate(
                    $hash,
                    "monthly_" . ( $_ + 1 ) . "_energy",
                    sprintf(
                        "%.2f", $decoded_json->[$_]->{'totalEnergyUsage'}
                    )
                );
                readingsBulkUpdate(
                    $hash,
                    "monthly_" . ( $_ + 1 ) . "_cost",
                    sprintf( "%.2f", $decoded_json->[$_]->{'totalCost'} )
                );
            }
            readingsEndUpdate( $hash, 1 );
            return;
        }

        if ( $param->{dpoint} eq 'getChargerConfiguration' ) {
            readingsBeginUpdate($hash);
            readingsBulkUpdate( $hash, "isEnabled",
                $decoded_json->{isEnabled} );
            readingsBulkUpdate( $hash, "isCablePermanentlyLocked",
                $decoded_json->{lockCablePermanently} );
            readingsBulkUpdate( $hash, "isAuthorizationRequired",
                $decoded_json->{authorizationRequired} );
            readingsBulkUpdate( $hash, "isRemoteStartRequired",
                $decoded_json->{remoteStartRequired} );
            readingsBulkUpdate( $hash, "isSmartButtonEnabled",
                $decoded_json->{smartButtonEnabled} );
            readingsBulkUpdate( $hash, "wiFiSSID", $decoded_json->{wiFiSSID} );
            readingsBulkUpdate( $hash, "phaseModeId",
                $decoded_json->{phaseMode} );
            readingsBulkUpdate( $hash, "phaseMode",
                $phaseModes{ $decoded_json->{phaseMode} } );
            readingsBulkUpdate(
                $hash,
                "isLocalAuthorizationRequired",
                $decoded_json->{localAuthorizationRequired}
            );
            readingsBulkUpdate( $hash, "maxChargerCurrent",
                $decoded_json->{maxChargerCurrent} );
            readingsBulkUpdate( $hash, "ledStripBrightness",
                $decoded_json->{ledStripBrightness} );

            #readingsBulkUpdate( $hash, "charger_offlineChargingMode",
            #    $decoded_json->{offlineChargingMode} );
            #readingsBulkUpdate( $hash, "charger_circuitMaxCurrentP1",
            #    $decoded_json->{circuitMaxCurrentP1} );
            #readingsBulkUpdate( $hash, "charger_circuitMaxCurrentP2",
            #    $decoded_json->{circuitMaxCurrentP2} );
            #readingsBulkUpdate( $hash, "charger_circuitMaxCurrentP3",
            #    $decoded_json->{circuitMaxCurrentP3} );
            #readingsBulkUpdate( $hash, "charger_enableIdleCurrent",
            #    $decoded_json->{enableIdleCurrent} );
            #readingsBulkUpdate(
            #    $hash,
            #    "charger_limitToSinglePhaseCharging",
            #    $decoded_json->{limitToSinglePhaseCharging}
            #);

            #readingsBulkUpdate( $hash, "charger_localNodeType",
            #    $decoded_json->{localNodeType} );

            #readingsBulkUpdate( $hash, "charger_localRadioChannel",
            #    $decoded_json->{localRadioChannel} );
            #readingsBulkUpdate( $hash, "charger_localShortAddress",
            #    $decoded_json->{localShortAddress} );
            #readingsBulkUpdate(
            #    $hash,
            #    "charger_localParentAddrOrNumOfNodes",
            #    $decoded_json->{localParentAddrOrNumOfNodes}
            #);
            #readingsBulkUpdate(
            #    $hash,
            #    "charger_localPreAuthorizeEnabled",
            #    $decoded_json->{localPreAuthorizeEnabled}
            #);
            #readingsBulkUpdate(
            #    $hash,
            #    "charger_allowOfflineTxForUnknownId",
            #    $decoded_json->{allowOfflineTxForUnknownId}
            #);
            #readingsBulkUpdate( $hash, "chargingSchedule",
            #    $decoded_json->{chargingSchedule} );
            readingsBulkUpdate( $hash, "lastResponse",
                'OK - getChargerConfig', 1 );
            readingsEndUpdate( $hash, 1 );
            return;
        }

        if ( $param->{dpoint} eq 'getCurrentSession' ) {
            readingsBeginUpdate($hash);
            readingsBulkUpdate( $hash, "session_energy",
                sprintf( "%.2f", $decoded_json->{sessionEnergy} ) );
            $value =
              defined $decoded_json->{sessionStart}
              ? _transcodeDate( $decoded_json->{sessionStart} )
              : 'N/A';
            readingsBulkUpdate( $hash, "session_start", $value );
            $value =
              defined $decoded_json->{sessionEnd}
              ? _transcodeDate( $decoded_json->{sessionEnd} )
              : 'N/A';
            readingsBulkUpdate( $hash, "session_end", $value );
            readingsBulkUpdate(
                $hash,
                "session_chargeDurationInSeconds",
                $decoded_json->{chargeDurationInSeconds}
            );
            $value =
              defined $decoded_json->{firstEnergyTransferPeriodStart}
              ? _transcodeDate(
                $decoded_json->{firstEnergyTransferPeriodStart} )
              : 'N/A';
            readingsBulkUpdate( $hash, "session_firstEnergyTransfer", $value );
            $value =
              defined $decoded_json->{lastEnergyTransferPeriodStart}
              ? _transcodeDate( $decoded_json->{lastEnergyTransferPeriodStart} )
              : 'N/A';
            readingsBulkUpdate( $hash, "session_lastEnergyTransfer", $value );
            readingsBulkUpdate( $hash, "session_pricePerKWH",
                $decoded_json->{pricePrKwhIncludingVat} );
            readingsBulkUpdate( $hash, "session_chargingCost",
                sprintf( "%.2f", $decoded_json->{costIncludingVat} ) );
            readingsBulkUpdate( $hash, "session_id",
                $decoded_json->{sessionId} );
            readingsBulkUpdate( $hash, "lastResponse",
                'OK - getCurrentSession', 1 );
            readingsEndUpdate( $hash, 1 );
            return;
        }

        if ( $param->{dpoint} eq 'getChargerSite' ) {
            readingsBeginUpdate($hash);
            readingsBulkUpdate( $hash, "cost_perKWh",
                $decoded_json->{costPerKWh} );
            readingsBulkUpdate( $hash, "cost_perKwhExcludeVat",
                $decoded_json->{costPerKwhExcludeVat} );
            readingsBulkUpdate( $hash, "cost_vat", $decoded_json->{vat} );
            readingsBulkUpdate( $hash, "cost_currency",
                $decoded_json->{currencyId} );

#readingsBulkUpdate( $hash, "site_ratedCurrent", $decoded_json->{ratedCurrent} );
#readingsBulkUpdate( $hash, "site_createdOn",    $decoded_json->{createdOn} );
#readingsBulkUpdate( $hash, "site_updatedOn",    $decoded_json->{updatedOn} );
            readingsBulkUpdate( $hash, "lastResponse",
                'OK - getChargerSite', 1 );
            readingsEndUpdate( $hash, 1 );
            return;
        }

        if ( $param->{dpoint} eq 'getChargerState' ) {
            readingsBeginUpdate($hash);
            readingsBulkUpdate( $hash, "operationModeCode",
                $decoded_json->{chargerOpMode} );
            readingsBulkUpdate( $hash, "operationMode",
                $operationModes{ $decoded_json->{chargerOpMode} } );
            readingsBulkUpdate( $hash, "power",
                sprintf( "%.2f", $decoded_json->{totalPower} ) );
            readingsBulkUpdate( $hash, "kWhInSession",
                sprintf( "%.2f", $decoded_json->{sessionEnergy} ) );
            readingsBulkUpdate( $hash, "phase", $decoded_json->{outputPhase} );
            readingsBulkUpdate( $hash, "latestPulse",
                _transcodeDate( $decoded_json->{latestPulse} ) );
            readingsBulkUpdate( $hash, "current",
                $decoded_json->{outputCurrent} );
            readingsBulkUpdate( $hash, "dynamicCurrent",
                $decoded_json->{dynamicChargerCurrent} );
            readingsBulkUpdate( $hash, "reasonCodeForNoCurrent",
                $decoded_json->{reasonForNoCurrent} );
            readingsBulkUpdate( $hash, "reasonForNoCurrent",
                $reasonsForNoCurrent{ $decoded_json->{reasonForNoCurrent} } );
            readingsBulkUpdate( $hash, "errorCode",
                $decoded_json->{errorCode} );
            readingsBulkUpdate( $hash, "fatalErrorCode",
                $decoded_json->{fatalErrorCode} );
            readingsBulkUpdate( $hash, "lifetimeEnergy",
                sprintf( "%.2f", $decoded_json->{lifetimeEnergy} ) );
            readingsBulkUpdate( $hash, "online", $decoded_json->{isOnline} );
            readingsBulkUpdate( $hash, "voltage",
                sprintf( "%.2f", $decoded_json->{voltage} ) );
            readingsBulkUpdate( $hash, "wifi_rssi", $decoded_json->{wiFiRSSI} );
            readingsBulkUpdate( $hash, "wifi_apEnabled",
                $decoded_json->{wiFiAPEnabled} );
            readingsBulkUpdate( $hash, "cell_rssi", $decoded_json->{cellRSSI} );
            readingsBulkUpdate( $hash, "lastResponse",
                'OK - getChargerState', 1 );
            readingsEndUpdate( $hash, 1 );
            return;
        }

        $decoded_json = $decoded_json->[0] if ref($decoded_json) eq "ARRAY";
        readingsSingleUpdate( $hash, "lastResponse",
            'OK - Action: ' . $commandCodes{ $decoded_json->{commandId} }, 1 )
          if defined $decoded_json->{commandId};
        readingsSingleUpdate(
            $hash,
            "lastResponse",
            'ERROR: '
              . $decoded_json->{title} . ' ('
              . $decoded_json->{status} . ')',
            1
          )
          if defined $decoded_json->{status} and defined $decoded_json->{title};
        return;
    }
    else {
        readingsSingleUpdate( $hash, "lastResponse", 'OK - empty', 1 );
        return;
    }

    if ($@) {
        readingsSingleUpdate( $hash, "lastResponse",
            'ERROR while deconding response: ' . $@, 1 );
        Log3 $name, 5, 'Failure decoding: ' . $@;
    }

    return;
}

sub Processing_DpointGetChargers {
    my $hash         = shift;
    my $decoded_json = shift;

    my $site    = $decoded_json->[0];
    my $circuit = $site->{circuits}->[0];
    my $charger = $circuit->{chargers}->[0];

    readingsBeginUpdate($hash);
    my $chargerId = $charger->{id};
    readingsBulkUpdate( $hash, "site_id",      $site->{id} );
    readingsBulkUpdate( $hash, "site_key",     $site->{siteKey} );
    readingsBulkUpdate( $hash, "charger_id",   $chargerId );
    readingsBulkUpdate( $hash, "charger_name", $charger->{name} );
    readingsBulkUpdate( $hash, "lastResponse", 'OK - getReaders', 1 );
    readingsEndUpdate( $hash, 1 );

    WriteToCloudAPI( $hash, 'getChargerConfiguration', 'GET' );

    return;
}

sub Processing_DpointGetChargerSessionsDaily {
    my $hash         = shift;
    my $decoded_json = shift;

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

    Log3 $name, 5, 'Evaluating getChargerSessionsDaily';

    my @x = $decoded_json;
    my @a = ( -5 .. -1 );

    readingsBeginUpdate($hash);
    for (@a) {
        Log3 $name, 5, 'laeuft noch: ' . $_;
        readingsBulkUpdate(
            $hash,
            "daily_" . ( $_ + 1 ) . "_energy",
            sprintf( "%.2f", $decoded_json->[$_]->{'totalEnergyUsage'} )
        );
        readingsBulkUpdate(
            $hash,
            "daily_" . ( $_ + 1 ) . "_cost",
            sprintf( "%.2f", $decoded_json->[$_]->{'totalCost'} )
        );
    }

    readingsEndUpdate( $hash, 1 );

    return;
}

sub _loadToken {
    my $hash          = shift;
    my $name          = $hash->{NAME};
    my $tokenLifeTime = $hash->{TOKEN_LIFETIME};
    $tokenLifeTime = 0 if ( !defined $tokenLifeTime || $tokenLifeTime eq '' );
    my $token;

    $token = $hash->{'.TOKEN'};

    if ( $@ || $tokenLifeTime < gettimeofday() ) {
        Log3 $name, 5,
          "EaseeWallbox $name" . ": "
          . "Error while loading: $@ ,requesting new one"
          if $@;
        Log3 $name, 5,
          "EaseeWallbox $name" . ": " . "Token is expired, requesting new one"
          if $tokenLifeTime < gettimeofday();
        $token = _newTokenRequest($hash);
    }
    else {
        Log3 $name, 5,
            "EaseeWallbox $name" . ": "
          . "Token expires at "
          . localtime($tokenLifeTime);

        # if token is about to expire, refresh him
        if ( ( $tokenLifeTime - 3700 ) < gettimeofday() ) {
            Log3 $name, 5,
              "EaseeWallbox $name" . ": "
              . "Token will expire soon, refreshing";
            $token = _tokenRefresh($hash);
        }
    }

    $token = $token ? $token : undef;
    return $token;
}

sub _newTokenRequest {
    my $hash     = shift;
    my $name     = $hash->{NAME};
    my $password = _decrypt( InternalVal( $name, 'Password', undef ) );
    my $username = InternalVal( $name, 'Username', undef );

    Log3 $name, 5, "EaseeWallbox $name" . ": " . "calling NewTokenRequest()";

    my $data = {
        userName => $username,
        password => $password,
    };

    my $param = {
        url     => $hash->{APIURI} . $dpoints{getOAuthToken},
        header  => { "Content-Type" => "application/json" },
        method  => 'POST',
        timeout => 5,
        hash    => $hash,
        data    => encode_json $data
    };
    Log3 $name, 5, 'Request: ' . Dumper($param);

    #Log3 $name, 5, 'Blocking GET: ' . Dumper($param);
    #Log3 $name, $reqDebug, "EaseeWallbox $name" . ": " . "Request $AuthURL";
    my ( $err, $returnData ) = HttpUtils_BlockingGet($param);

    if ( $err ne "" ) {
        Log3 $name, 3,
            "EaseeWallbox $name" . ": "
          . "NewTokenRequest: Error while requesting "
          . $param->{url}
          . " - $err";
    }
    elsif ( $returnData ne "" ) {
        Log3 $name, 5, "url " . $param->{url} . " returned: $returnData";
        my $decoded_data = eval { decode_json($returnData) };
        if ($@) {
            Log3 $name, 3, "EaseeWallbox $name" . ": "
              . "NewTokenRequest: decode_json failed, invalid json. error: $@ ";
        }
        else {
            #write token data in hash
            if ( defined($decoded_data) ) {
                $hash->{'.TOKEN'} = $decoded_data;
            }

            # token lifetime management
            if ( defined($decoded_data) ) {
                $hash->{TOKEN_LIFETIME} =
                  gettimeofday() + $decoded_data->{'expiresIn'};
            }
            $hash->{TOKEN_LIFETIME_HR} = localtime( $hash->{TOKEN_LIFETIME} );
            Log3 $name, 5,
                "EaseeWallbox $name" . ": "
              . "Retrived new authentication token successfully. Valid until "
              . localtime( $hash->{TOKEN_LIFETIME} );

# $hash->{STATE} = "reachable";       # niemals $hash->{STATE} über demn Hash direkt zuweisen
            readingsSingleUpdate( $hash, 'state', 'reachable', 1 );
            return $decoded_data;
        }
    }
    return;
}

sub _tokenRefresh {
    my $hash = shift;
    my $name = $hash->{NAME};

    my $Token = undef;

    # load token
    $Token = $hash->{'.TOKEN'};

    my $data = {
        accessToken  => $Token->{'accessToken'},
        refreshToken => $Token->{'refreshToken'}
    };

    my $param = {
        url     => $hash->{APIURI} . $dpoints{getRefreshToken},
        header  => { "Content-Type" => "application/json" },
        method  => 'POST',
        timeout => 5,
        hash    => $hash,
        data    => encode_json $data
    };

    Log3 $name, 5, 'Request: ' . Dumper($param);

    #Log3 $name, 5, 'Blocking GET TokenRefresh: ' . Dumper($param);
    #Log3 $name, $reqDebug, "EaseeWallbox $name" . ": " . "Request $AuthURL";
    my ( $err, $returnData ) = HttpUtils_BlockingGet($param);

    if ( $err ne "" ) {
        Log3 $name, 3,
            "EaseeWallbox $name" . ": "
          . "TokenRefresh: Error in token retrival while requesting "
          . $param->{url}
          . " - $err";

        # $hash->{STATE} = "error";
        readingsSingleUpdate( $hash, 'state', 'error', 1 );
    }

    elsif ( $returnData ne "" ) {
        Log3 $name, 5, "url " . $param->{url} . " returned: $returnData";
        my $decoded_data = eval { decode_json($returnData); };

        if ($@) {
            Log3 $name, 3,
              "EaseeWallbox $name" . ": "
              . "TokenRefresh: decode_json failed, invalid json. error:$@\n"
              if $@;

            # $hash->{STATE} = "error";
            readingsSingleUpdate( $hash, 'state', 'error', 1 );
        }
        else {
            #write token data in file
            if ( defined($decoded_data) ) {
                $hash->{'.TOKEN'} = $decoded_data;

            }

            # token lifetime management
            $hash->{TOKEN_LIFETIME} =
              gettimeofday() + $decoded_data->{'expires_in'};
            $hash->{TOKEN_LIFETIME_HR} = localtime( $hash->{TOKEN_LIFETIME} );
            Log3 $name, 5,
                "EaseeWallbox $name" . ": "
              . "TokenRefresh: Refreshed authentication token successfully. Valid until "
              . localtime( $hash->{TOKEN_LIFETIME} );

            # $hash->{STATE} = "reachable";
            readingsSingleUpdate( $hash, 'state', 'reachable', 1 );
            return $decoded_data;
        }
    }
    return;
}

sub _encrypt {
    my ($decoded) = @_;
    my $key = getUniqueId();
    my $encoded;

    return $decoded
      if ( $decoded =~ /crypt:/x )
      ; # Regular expression without "/x" flag. See page 236 of PBP (RegularExpressions::RequireExtendedFormatting)

    for my $char ( split //, $decoded ) {
        my $encode = chop($key);
        $encoded .= sprintf( "%.2x", ord($char) ^ ord($encode) );
        $key = $encode . $key;
    }

    return 'crypt:' . $encoded;
}

sub _decrypt {
    my ($encoded) = @_;
    my $key = getUniqueId();
    my $decoded;

    return $encoded
      if ( $encoded !~ /crypt:/x )
      ; # Regular expression without "/x" flag. See page 236 of PBP (RegularExpressions::RequireExtendedFormatting)

    $encoded = $1
      if ( $encoded =~ /crypt:(.*)/x )
      ; # Regular expression without "/x" flag. See page 236 of PBP (RegularExpressions::RequireExtendedFormatting)

    for my $char ( map { pack( 'C', hex($_) ) } ( $encoded =~ /(..)/xg ) )
    { # Regular expression without "/x" flag. See page 236 of PBP (RegularExpressions::RequireExtendedFormatting)
        my $decode = chop($key);
        $decoded .= chr( ord($char) ^ ord($decode) );
        $key = $decode . $key;
    }

    return $decoded;
}

1;

sub _transcodeDate {
    my $datestr = shift;
    Log3 'EaseeWallbox', 5, 'date to parse: ' . $datestr;
    my $strp = DateTime::Format::Strptime->new(
        on_error => 'croak',
        pattern  => '%Y-%m-%dT%H:%M:%S%z'
    );
    my $dt = $strp->parse_datetime($datestr);
    $dt->set_time_zone('Europe/Berlin');

    return $dt->strftime('%Y-%m-%d %H:%M:%S');
}

1;    # Ein Modul muss immer mit 1; enden

=pod
=begin html


=end html

=cut