package main; use strict; use warnings; use Data::Dumper; use utf8; use Encode qw( encode_utf8 ); use HttpUtils; use JSON; my %EaseeWallbox_gets = ( update => "noArg", health => "noArg", baseData => "noArg", chargers => "noArg", sites => "noArg", profile => "noArg", config => "noArg", ); my %EaseeWallbox_sets = ( startCharging => "", stopCharging => "", pauseCharging => "", resumeCharging => "", toggleCharging => "", interval => "", refreshToken => "noArg", cableLock => "true,false", reboot => "noArg", updateFirmware => "noArg", enableSmartCharging => "true,false", overrideChargingSchedule => "", pairRfidTag => "", pricePerKWH => "", activateTimer => "", deactivateTimer => "", ); my %url = ( getOAuthToken => 'https://api.easee.cloud/api/accounts/login', getRefreshToken => 'https://api.easee.cloud/api/accounts/refresh_token', getProfile => 'https://api.easee.cloud/api/accounts/profile', getChargingSession => 'https://api.easee.cloud/api/chargers/#ChargerID#/sessions/ongoing', getChargers => 'https://api.easee.cloud/api/accounts/chargers', getProducts => 'https://api.easee.cloud/api/accounts/products?userId=#UserId#', getChargerSite => 'https://api.easee.cloud/api/chargers/#ChargerID#/site', getChargerDetails => 'https://api.easee.cloud/api/chargers/#ChargerID#/details', getChargerConfiguration => 'https://api.easee.cloud/api/chargers/#ChargerID#/config', getChargerSessionsMonthly => 'https://api.easee.cloud/api/sessions/charger/#ChargerID#/monthly', getChargerSessionsDaily => 'https://api.easee.cloud/api/sessions/charger/#ChargerID#/daily', getChargerState => 'https://api.easee.cloud/api/chargers/#ChargerID#/state', getCurrentSession => 'https://api.easee.cloud/api/chargers/#ChargerID#/sessions/ongoing', setCableLockState => 'https://api.easee.cloud/api/chargers/#ChargerID#/commands/lock_state', setReboot => 'https://api.easee.cloud/api/chargers/#ChargerID#/commands/reboot', setUpdateFirmware => 'https://api.easee.cloud/api/chargers/#ChargerID#/commands/update_firmware', setEnableSmartCharging => 'https://api.easee.cloud/api/chargers/#ChargerID#/commands/smart_charging', setStartCharging => 'https://api.easee.cloud/api/chargers/#ChargerID#/commands/start_charging', setStopCharging => 'https://api.easee.cloud/api/chargers/#ChargerID#/commands/stop_charging', setPauseCharging => 'https://api.easee.cloud/api/chargers/#ChargerID#/commands/pause_charging', setResumeCharging => 'https://api.easee.cloud/api/chargers/#ChargerID#/commands/resume_charging', setToggleCharging => 'https://api.easee.cloud/api/chargers/#ChargerID#/commands/toggle_charging', setOverrideChargingSchedule => 'https://api.easee.cloud/api/chargers/#ChargerID#/commands/override_schedule', setPairRFIDTag => 'https://api.easee.cloud/api/chargers/#ChargerID#/commands/set_rfid_pairing_mode_async', changeChargerSettings => 'https://api.easee.cloud/api/chargers/#ChargerID#/settings', setChargingPrice => 'https://api.easee.cloud/api/sites/#SiteID#/price', ); my %reasonForNoCurrent = ( 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 %phaseMode = ( 1 => 'Locked to single phase', 2 => 'Auto', 3 => 'Locked to three phase', ); my %operationMode = ( 1 => "Standby", 2 => "Paused", 3 => 'Charging', 4 => 'Completed', 5 => 'Error', 6 => 'CarConnected' ); #Private function to evaluate command-lists ############################# sub EaseeWallbox_getCmdList ($$$) { my ( $hash, $cmd, $commands ) = @_; my %cmdArray = %$commands; my $name = $hash->{NAME}; #return, if cmd is valid return undef 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) ); $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) ); #Logging makes me crazy... #Log3 ($name, 5, "parse cmd-table - Set:$mySet, Option:$myOpt, RetVal:$retVal"); } if ( !defined($retVal) ) { $retVal = "error while parsing set-table"; } else { $retVal = "Unknown argument $cmd, choose one of " . $retVal; } return $retVal; } sub EaseeWallbox_Initialize($) { my ($hash) = @_; $hash->{DefFn} = 'EaseeWallbox_Define'; $hash->{UndefFn} = 'EaseeWallbox_Undef'; $hash->{SetFn} = 'EaseeWallbox_Set'; $hash->{GetFn} = 'EaseeWallbox_Get'; $hash->{AttrFn} = 'EaseeWallbox_Attr'; $hash->{ReadFn} = 'EaseeWallbox_Read'; $hash->{WriteFn} = 'EaseeWallbox_Write'; $hash->{Clients} = ':EaseeWallbox:'; $hash->{MatchList} = { '1:EaseeWallbox' => '^EaseeWallbox;.*' }; $hash->{AttrList} = 'expertMode:yes,no ' . 'ledStuff:yes,no ' . 'SmartCharging:true,false ' . $readingFnAttributes; Log 3, "EaseeWallbox module initialized."; } sub EaseeWallbox_Define($$) { my ( $hash, $def ) = @_; my @param = split( "[ \t]+", $def ); my $name = $hash->{NAME}; 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 EaseeWallbox [Interval]"; Log3 $name, 1, "EaseeWallbox $name: " . $errmsg; return $errmsg; } #Check if the username is an email address if ( $param[2] =~ /^.+@.+$/ ) { my $username = $param[2]; $hash->{Username} = $username; } else { $errmsg = "specify valid email address within the field username. Format: define EaseeWallbox [interval]"; Log3 $name, 1, "EaseeWallbox $name: " . $errmsg; return $errmsg; } #Take password and use custom encryption. # Encryption is taken from fitbit / withings module my $password = EaseeWallbox_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+$/ ) { $interval = $param[4]; } else { $errmsg = "Specify valid integer value for interval. Whole numbers > 5 only. Format: define EaseeWallbox [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 EaseeWallbox_UpdateBaseData($hash); EaseeWallbox_RefreshData($hash); Log3 $name, 1, sprintf("EaseeWallbox_Define %s: Starting timer with interval %s", $name, InternalVal($name,'INTERVAL', undef)); InternalTimer(gettimeofday()+ InternalVal($name,'INTERVAL', undef), "EaseeWallbox_UpdateDueToTimer", $hash) if (defined $hash); return undef; } sub EaseeWallbox_Undef($$) { my ( $hash, $arg ) = @_; RemoveInternalTimer($hash); return undef; } sub EaseeWallbox_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 = EaseeWallbox_getCmdList( $hash, $opt, \%EaseeWallbox_gets ); return $cmdTemp if ( defined($cmdTemp) ); $hash->{LOCAL} = 1; EaseeWallbox_GetChargers($hash) if $opt eq "chargers"; EaseeWallbox_GetChargerConfig($hash) if $opt eq "config"; EaseeWallbox_GetChargerSite($hash) if $opt eq "sites"; EaseeWallbox_RefreshData($hash) if $opt eq "update"; EaseeWallbox_UpdateBaseData($hash) if $opt eq 'baseData'; delete $hash->{LOCAL}; return undef; } sub EaseeWallbox_Set($@) { my ( $hash, $name, @param ) = @_; return '"set $name" needs at least one argument' if ( int(@param) < 1 ); my $opt = shift @param; my $value = join( "", @param ); #create response, if cmd is wrong or gui asks my $cmdTemp = EaseeWallbox_getCmdList( $hash, $opt, \%EaseeWallbox_sets ); return $cmdTemp if ( defined($cmdTemp) ); if ( $opt eq "deactivateTimer" ) { RemoveInternalTimer($hash); Log3 $name, 1, "EaseeWallbox_Set $name: Stopped the timer to automatically update readings"; readingsSingleUpdate( $hash, 'state', 'Initialized', 0 ); return undef; } elsif ( $opt eq "activateTimer" ) { #Update once manually and then start the timer RemoveInternalTimer($hash); $hash->{LOCAL} = 1; EaseeWallbox_RefreshData($hash); delete $hash->{LOCAL}; InternalTimer(gettimeofday()+ InternalVal($name,'INTERVAL', undef), "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" ) { 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; } else { $hash->{LOCAL} = 1; EaseeWallbox_ExecuteParameterlessCommand( $hash, "setStartCharging" ) if $opt eq "startCharging"; EaseeWallbox_ExecuteParameterlessCommand( $hash, "setStopCharging" ) if $opt eq 'stopCharging'; EaseeWallbox_ExecuteParameterlessCommand( $hash, "setPauseCharging" ) if $opt eq 'pauseCharging'; EaseeWallbox_ExecuteParameterlessCommand( $hash, "setResumeCharging" ) if $opt eq 'resumeCharging'; EaseeWallbox_ExecuteParameterlessCommand( $hash, "setToggleCharging" ) if $opt eq 'toggleCharging'; EaseeWallbox_ExecuteParameterlessCommand( $hash, "setUpdateFirmware" ) if $opt eq 'updateFirmware'; EaseeWallbox_ExecuteParameterlessCommand( $hash, "setOverrideChargingSchedule" ) if $opt eq 'overrideChargingSchedule'; EaseeWallbox_ExecuteParameterlessCommand( $hash, "setPairRFIDTag" ) if $opt eq 'pairRfidTag'; EaseeWallbox_ExecuteParameterlessCommand( $hash, "setReboot" ) if $opt eq 'reboot'; EaseeWallbox_ExecuteParameterlessCommand( $hash, "toBeDone" ) if $opt eq 'enableSmartCharging'; EaseeWallbox_SetCableLock( $hash, shift @param ) if $opt eq 'cableLock'; EaseeWallbox_SetPrice( $hash, shift @param ) if $opt eq 'pricePerKWH'; EaseeWallbox_LoadToken($hash) if $opt eq 'refreshToken'; delete $hash->{LOCAL}; } readingsSingleUpdate( $hash, 'state', 'Initialized', 0 ); return undef; } sub EaseeWallbox_RefreshData($){ my $hash = shift; my $name = $hash->{NAME}; EaseeWallbox_GetChargerSite($hash); EaseeWallbox_RequestChargerState($hash); EaseeWallbox_RequestCurrentSession($hash); readingsSingleUpdate( $hash, "state", sprintf('%s (%.2f)
Current Session: %.2f kWH (%.2f€)', ReadingsVal($name,"operationMode","N/A"), ReadingsVal($name,"power","0"), ReadingsVal($name,"kWhInSession","0"), ReadingsVal($name,"session_chargingCost","0")), 1 ); } sub EaseeWallbox_UpdateBaseData($){ my $hash = shift; EaseeWallbox_GetChargers($hash); EaseeWallbox_GetChargerConfig($hash); EaseeWallbox_RefreshData($hash); } sub EaseeWallbox_LoadToken { my $hash = shift; my $name = $hash->{NAME}; my $tokenLifeTime = $hash->{TOKEN_LIFETIME}; $tokenLifeTime = 0 if ( !defined $tokenLifeTime || $tokenLifeTime eq '' ); my $Token = undef; $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 = EaseeWallbox_NewTokenRequest($hash); } else { Log3 $name, 5, "EaseeWallbox $name" . ": " . "Token expires at " . localtime($tokenLifeTime); # if token is about to expire, refresh him if ( ( $tokenLifeTime - 45 ) < gettimeofday() ) { Log3 $name, 5, "EaseeWallbox $name" . ": " . "Token will expire soon, refreshing"; $Token = EaseeWallbox_TokenRefresh($hash); } } return $Token if $Token; } sub EaseeWallbox_NewTokenRequest { my $hash = shift; my $name = $hash->{NAME}; my $password = EaseeWallbox_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 => $url{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->{'expires_in'}; } $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"; return $decoded_data; } } return; } sub EaseeWallbox_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 => $url{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"; } 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"; } 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"; return $decoded_data; } } return; } sub EaseeWallbox_httpSimpleOperationOAuth($$$;$) { my ( $hash, $url, $operation, $message ) = @_; my ( $json, $err, $data, $decoded ); my $name = $hash->{NAME}; my $CurrentTokenData = EaseeWallbox_LoadToken($hash); Log3 $name, 3, "$CurrentTokenData->{'tokenType'} $CurrentTokenData->{'accessToken'}"; my $request = { url => $url, header => { "Content-Type" => "application/json;charset=UTF-8", "Authorization" => "$CurrentTokenData->{'tokenType'} $CurrentTokenData->{'accessToken'}" }, method => $operation, timeout => 6, hideurl => 1 }; $request->{data} = $message if ( defined $message ); Log3 $name, 5, 'Request: ' . Dumper($request); ( $err, $data ) = HttpUtils_BlockingGet($request); $json = "" if ( !$json ); $data = "" if ( !$data ); Log3 $name, 4, "FHEM -> EaseeWallbox: " . $url; Log3 $name, 4, "FHEM -> EaseeWallbox: " . $message if ( defined $message ); Log3 $name, 4, "EaseeWallbox -> FHEM: " . $data if ( defined $data ); Log3 $name, 4, "EaseeWallbox -> FHEM: Got empty response." if ( not defined $data ); Log3 $name, 5, '$err: ' . $err; Log3 $name, 5, "method: " . $operation; Log3 $name, 2, "Something gone wrong" if ( $data =~ "/EaseeWallboxMode/" ); $err = 1 if ( $data =~ "/EaseeWallboxMode/" ); if ( defined $data and ( not $data eq '' ) and $operation ne 'DELETE' ) { eval { $decoded = decode_json($data) if ( !$err ); Log3 $name, 5, 'Decoded: ' . Dumper($decoded); return $decoded; } or do { Log3 $name, 5, 'Failure decoding: ' . $@; } } else { return undef; } } sub EaseeWallbox_ExecuteParameterlessCommand($$) { my ( $hash, $template ) = @_; EaseeWallbox_ExecuteCommand($hash, 'POST', $template, undef) } sub EaseeWallbox_ExecuteCommand($@) { my ( $hash, $method, $template, $message ) = @_; my $name = $hash->{NAME}; my $urlTemplate = $url{$template}; if ( not defined $hash ) { Log3 'EaseeWallbox', 1, "Error on EaseeWallbox_ExecuteCommand. Missing hash variable"; return undef; } #Check if chargerID is required in URL and replace or alert. if ( $urlTemplate =~ m/#ChargerID#/ ) { my $chargerId = ReadingsVal( $name, 'charger_id', undef ); if ( not defined $chargerId ) { Log3 'EaseeWallbox', 1, "Error on EaseeWallbox_ExecuteCommand. Missing charger_id. Please ensure basic data is available."; return undef; } $urlTemplate =~ s/#ChargerID#/$chargerId/g; } #Check if siteID is required in URL and replace or alert. if ( $urlTemplate =~ m/#SiteID#/ ) { my $siteId = ReadingsVal( $name, 'site_id', undef ); if ( not defined $siteId ) { Log3 'EaseeWallbox', 1, "Error on EaseeWallbox_ExecuteCommand. Missing site_id. Please ensure basic data is available."; return undef; } $urlTemplate =~ s/#SiteID#/$siteId/g; } Log3 $name, 4, "EaseeWallbox_ExecuteCommand will call Easee API for blocking value update. Name: $name"; my $d = EaseeWallbox_httpSimpleOperationOAuth( $hash, $urlTemplate, $method, encode_json \%$message ); } sub EaseeWallbox_SetCableLock($$) { my ( $hash, $value ) = @_; my %message; $message{'state'} = $value; EaseeWallbox_ExecuteCommand($hash, "POST", "setCableLockState", \%message); } sub EaseeWallbox_SetPrice($$) { my ( $hash, $value ) = @_; my %message; $message{'currencyId'} = "EUR"; $message{'vat'} = "19"; $message{'costPerKWh'} = $value; EaseeWallbox_ExecuteCommand($hash, "POST", "setChargingPrice", \%message); } sub EaseeWallbox_Attr(@) { return undef; } sub EaseeWallbox_GetChargers($) { my ($hash) = @_; my $name = $hash->{NAME}; if ( not defined $hash ) { my $msg = "Error on EaseeWallbox_GetChargers. Missing hash variable"; Log3 'EaseeWallbox', 1, $msg; return $msg; } my $readTemplate = $url{"getChargers"}; my $d = EaseeWallbox_httpSimpleOperationOAuth( $hash, $readTemplate, 'GET' ); if ( defined $d && ref($d) eq "HASH" && defined $d->{errors} ) { log 1, Dumper $d; readingsSingleUpdate( $hash, "Error: $d->{errors}[0]->{code} / $d->{errors}[0]->{title}", 'Undefined', 1 ); return undef; } else { readingsBeginUpdate($hash); my $site = $d->[0]; my $circuit = $site->{circuits}->[0]; my $charger = $circuit->{chargers}->[0]; readingsBeginUpdate($hash); my $chargerId = $charger->{id}; readingsBulkUpdate( $hash, "charger_id", $chargerId ); readingsBulkUpdate( $hash, "charger_name", $charger->{name} ); #readingsBulkUpdate( $hash, "charger_isTemporary", $charger->{isTemporary} ); #readingsBulkUpdate( $hash, "charger_createdOn", $charger->{createdOn} ); readingsEndUpdate( $hash, 1 ); $readTemplate = $url{"getChargerDetails"}; $readTemplate =~ s/#ChargerID#/$chargerId/g; $d = EaseeWallbox_httpSimpleOperationOAuth( $hash, $readTemplate, 'GET' ); if ( defined $d && ref($d) eq "HASH" && defined $d->{errors} ) { log 1, Dumper $d; readingsSingleUpdate( $hash, "Error: $d->{errors}[0]->{code} / $d->{errors}[0]->{title}", 'Undefined', 1 ); return undef; } else { readingsBeginUpdate($hash); readingsBulkUpdate( $hash, "product", $d->{product} ); readingsBulkUpdate( $hash, "pincode", $d->{pinCode} ); readingsBulkUpdate( $hash, "unitType", $d->{unitType} ); readingsEndUpdate( $hash, 1 ); } return undef; } } sub EaseeWallbox_GetChargerConfig($) { my ($hash) = @_; my $name = $hash->{NAME}; my $chargerId = ReadingsVal( $name, "charger_id", undef ); if ( not defined $chargerId ) { my $msg = "Error on EaseeWallbox_GetDevices. Missing Charger ID. Please get Chargers first."; Log3 'EaseeWallbox', 1, $msg; return $msg; } my $readTemplate = $url{"getChargerConfiguration"}; $readTemplate =~ s/#ChargerID#/$chargerId/g; my $d = EaseeWallbox_httpSimpleOperationOAuth( $hash, $readTemplate, 'GET' ); if ( defined $d && ref($d) eq "HASH" && defined $d->{errors} ) { log 1, Dumper $d; readingsSingleUpdate( $hash, 'state', "Error: $d->{errors}[0]->{code} / $d->{errors}[0]->{title}", 1 ); return undef; } else { readingsBeginUpdate($hash); readingsBulkUpdate( $hash, "charger_isEnabled", $d->{isEnabled} ); readingsBulkUpdate( $hash, "lockCablePermanently", $d->{lockCablePermanently} ); readingsBulkUpdate( $hash, "authorizationRequired", $d->{authorizationRequired} ); readingsBulkUpdate( $hash, "remoteStartRequired", $d->{remoteStartRequired} ); readingsBulkUpdate( $hash, "smartButtonEnabled", $d->{smartButtonEnabled} ); readingsBulkUpdate( $hash, "wiFiSSID", $d->{wiFiSSID} ); #readingsBulkUpdate( $hash, "charger_offlineChargingMode", # $d->{offlineChargingMode} ); #readingsBulkUpdate( $hash, "charger_circuitMaxCurrentP1", # $d->{circuitMaxCurrentP1} ); #readingsBulkUpdate( $hash, "charger_circuitMaxCurrentP2", # $d->{circuitMaxCurrentP2} ); #readingsBulkUpdate( $hash, "charger_circuitMaxCurrentP3", # $d->{circuitMaxCurrentP3} ); #readingsBulkUpdate( $hash, "charger_enableIdleCurrent", # $d->{enableIdleCurrent} ); #readingsBulkUpdate( # $hash, # "charger_limitToSinglePhaseCharging", # $d->{limitToSinglePhaseCharging} #); readingsBulkUpdate( $hash, "charger_phaseModeId", $d->{phaseMode} ); readingsBulkUpdate( $hash, "charger_phaseMode", $phaseMode{ $d->{phaseMode} } ); #readingsBulkUpdate( $hash, "charger_localNodeType", # $d->{localNodeType} ); readingsBulkUpdate( $hash, "localAuthorizationRequired", $d->{localAuthorizationRequired} ); #readingsBulkUpdate( $hash, "charger_localRadioChannel", # $d->{localRadioChannel} ); #readingsBulkUpdate( $hash, "charger_localShortAddress", # $d->{localShortAddress} ); #readingsBulkUpdate( # $hash, # "charger_localParentAddrOrNumOfNodes", # $d->{localParentAddrOrNumOfNodes} #); #readingsBulkUpdate( # $hash, # "charger_localPreAuthorizeEnabled", # $d->{localPreAuthorizeEnabled} #); #readingsBulkUpdate( # $hash, # "charger_allowOfflineTxForUnknownId", # $d->{allowOfflineTxForUnknownId} #); readingsBulkUpdate( $hash, "maxChargerCurrent", $d->{maxChargerCurrent} ); readingsBulkUpdate( $hash, "ledStripBrightness", $d->{ledStripBrightness} ); #readingsBulkUpdate( $hash, "chargingSchedule", # $d->{chargingSchedule} ); readingsEndUpdate( $hash, 1 ); return undef; } EaseeWallbox_RequestDeviceUpdate($hash); } sub EaseeWallbox_GetChargerSite($) { my ($hash) = @_; my $name = $hash->{NAME}; my $chargerId = ReadingsVal( $name, "charger_id", undef ); if ( not defined $chargerId ) { my $msg = "Error on EaseeWallbox_GetChargerSite. Missing Charger ID. Please get Chargers first."; Log3 'EaseeWallbox', 1, $msg; return $msg; } my $readTemplate = $url{"getChargerSite"}; $readTemplate =~ s/#ChargerID#/$chargerId/g; my $d = EaseeWallbox_httpSimpleOperationOAuth( $hash, $readTemplate, 'GET' ); if ( defined $d && ref($d) eq "HASH" && defined $d->{errors} ) { log 1, Dumper $d; readingsSingleUpdate( $hash, 'state', "Error: $d->{errors}[0]->{code} / $d->{errors}[0]->{title}", 1 ); return undef; } else { readingsBeginUpdate($hash); readingsBulkUpdate( $hash, "site_key", $d->{siteKey} ); readingsBulkUpdate( $hash, "site_id", $d->{id} ); readingsBulkUpdate( $hash, "cost_perKWh", $d->{costPerKWh} ); readingsBulkUpdate( $hash, "cost_perKwhExcludeVat", $d->{costPerKwhExcludeVat} ); readingsBulkUpdate( $hash, "cost_vat", $d->{vat} ); readingsBulkUpdate( $hash, "cost_currency", $d->{currencyId} ); #readingsBulkUpdate( $hash, "site_ratedCurrent", $d->{ratedCurrent} ); #readingsBulkUpdate( $hash, "site_createdOn", $d->{createdOn} ); #readingsBulkUpdate( $hash, "site_updatedOn", $d->{updatedOn} ); readingsEndUpdate( $hash, 1 ); return undef; } EaseeWallbox_RequestDeviceUpdate($hash); } sub EaseeWallbox_RequestCurrentSessionCallback($) { my ( $param, $err, $data ) = @_; my $hash = $param->{hash}; my $name = $hash->{NAME}; 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, "state", "ERROR", 1 ); return undef; } 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/" ); eval { my $d = decode_json($data) if ( !$err ); Log3 $name, 5, 'Decoded: ' . Dumper($d); readingsBeginUpdate($hash); readingsBulkUpdate( $hash, "session_energy", $d->{sessionEnergy} ); readingsBulkUpdate( $hash, "session_start", $d->{sessionStart} ); readingsBulkUpdate( $hash, "session_end", $d->{sessionEnd} ); readingsBulkUpdate( $hash, "session_chargeDurationInSeconds", $d->{chargeDurationInSeconds} ); readingsBulkUpdate( $hash, "session_firstEnergyTransfer", $d->{firstEnergyTransferPeriodStart} ); readingsBulkUpdate( $hash, "session_lastEnergyTransfer", $d->{lastEnergyTransferPeriodStart} ); readingsBulkUpdate( $hash, "session_pricePerKWH", $d->{pricePrKwhIncludingVat} ); readingsBulkUpdate( $hash, "session_chargingCost", $d->{costIncludingVat} ); readingsBulkUpdate( $hash, "session_id", $d->{sessionId} ); readingsEndUpdate( $hash, 1 ); return undef; } or do { Log3 $name, 5, 'Failure decoding: ' . $@; return undef; } } sub EaseeWallbox_RequestChargerStateCallback($) { my ( $param, $err, $data ) = @_; my $hash = $param->{hash}; my $name = $hash->{NAME}; 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, "state", "ERROR", 1 ); return undef; } Log3 $name, 3, "Received non-blocking data from EaseeWallbox regarding current state "; 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/" ); eval { my $d = decode_json($data) if ( !$err ); Log3 $name, 5, 'Decoded: ' . Dumper($d); readingsBeginUpdate($hash); readingsBulkUpdate( $hash, "operationModeCode", $d->{chargerOpMode} ); readingsBulkUpdate( $hash, "operationMode", $operationMode{ $d->{chargerOpMode} } ); readingsBulkUpdate( $hash, "power", $d->{totalPower} ); readingsBulkUpdate( $hash, "kWhInSession", $d->{sessionEnergy} ); readingsBulkUpdate( $hash, "phase", $d->{outputPhase} ); readingsBulkUpdate( $hash, "latestPulse", $d->{latestPulse} ); readingsBulkUpdate( $hash, "current", $d->{outputCurrent} ); readingsBulkUpdate( $hash, "dynamicCurrent", $d->{dynamicChargerCurrent} ); readingsBulkUpdate( $hash, "reasonCodeForNoCurrent", $d->{reasonForNoCurrent} ); readingsBulkUpdate( $hash, "reasonForNoCurrent", $reasonForNoCurrent{ $d->{reasonForNoCurrent} } ); readingsBulkUpdate( $hash, "errorCode", $d->{errorCode} ); readingsBulkUpdate( $hash, "fatalErrorCode", $d->{fatalErrorCode} ); readingsBulkUpdate( $hash, "lifetimeEnergy", $d->{lifetimeEnergy} ); readingsBulkUpdate( $hash, "online", $d->{isOnline} ); readingsBulkUpdate( $hash, "voltage", $d->{voltage} ); readingsBulkUpdate( $hash, "wifi_rssi", $d->{wiFiRSSI} ); readingsBulkUpdate( $hash, "wifi_apEnabled", $d->{wiFiAPEnabled} ); readingsBulkUpdate( $hash, "cell_rssi", $d->{cellRSSI} ); readingsEndUpdate( $hash, 1 ); return undef; } or do { Log3 $name, 5, 'Failure decoding: ' . $@; return undef; } } sub EaseeWallbox_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 ), "EaseeWallbox_UpdateDueToTimer", $hash ); readingsSingleUpdate( $hash, 'state', 'Polling', 0 ); } EaseeWallbox_RefreshData($hash); } sub EaseeWallbox_RequestCurrentSession($) { my ($hash) = @_; my $name = $hash->{NAME}; if ( not defined $hash ) { Log3 'EaseeWallbox', 1, "Error on EaseeWallbox_RequestCurrentSession. Missing hash variable"; return undef; } if ( not defined ReadingsVal( $name, 'charger_id', undef ) ) { Log3 'EaseeWallbox', 1, "Error on EaseeWallbox_RequestCurrentSession. Missing charger_id. Please fetch basic data first."; return undef; } my $chargerId = ReadingsVal( $name, "charger_id", undef ); $hash->{charger} = $chargerId; Log3 $name, 4, "EaseeWallbox_RequestCurrentSession Called for non-blocking value update. Name: $name"; my $readTemplate = $url{"getCurrentSession"}; $readTemplate =~ s/#ChargerID#/$chargerId/g; my $CurrentTokenData = EaseeWallbox_LoadToken($hash); my $token = "$CurrentTokenData->{'tokenType'} $CurrentTokenData->{'accessToken'}"; Log3 $name, 4, "token beeing used: " . $token; my $request = { url => $readTemplate, header => { "Content-Type" => "application/json;charset=UTF-8", "Authorization" => $token, }, method => 'GET', timeout => 5, hideurl => 1, callback => \&EaseeWallbox_RequestCurrentSessionCallback, hash => $hash }; Log3 $name, 5, 'NonBlocking Request: ' . Dumper($request); HttpUtils_NonblockingGet($request); } sub EaseeWallbox_RequestChargerState($) { my ($hash) = @_; my $name = $hash->{NAME}; if ( not defined $hash ) { Log3 'EaseeWallbox', 1, "Error on EaseeWallbox_RequestChargerState. Missing hash variable"; return undef; } if ( not defined ReadingsVal( $name, 'charger_id', undef ) ) { Log3 'EaseeWallbox', 1, "Error on EaseeWallbox_RequestChargerState. Missing charger_id. Please fetch basic data first."; return undef; } my $chargerId = ReadingsVal( $name, "charger_id", undef ); $hash->{charger} = $chargerId; Log3 $name, 4, "EaseeWallbox_RequestChargerState Called for non-blocking value update. Name: $name"; my $readTemplate = $url{"getChargerState"}; $readTemplate =~ s/#ChargerID#/$chargerId/g; my $CurrentTokenData = EaseeWallbox_LoadToken($hash); my $token = "$CurrentTokenData->{'tokenType'} $CurrentTokenData->{'accessToken'}"; Log3 $name, 4, "token beeing used: " . $token; my $request = { url => $readTemplate, header => { "Content-Type" => "application/json;charset=UTF-8", "Authorization" => $token, }, method => 'GET', timeout => 5, hideurl => 1, callback => \&EaseeWallbox_RequestChargerStateCallback, hash => $hash }; Log3 $name, 5, 'NonBlocking Request: ' . Dumper($request); HttpUtils_NonblockingGet($request); } sub EaseeWallbox_encrypt($) { my ($decoded) = @_; my $key = getUniqueId(); my $encoded; return $decoded if ( $decoded =~ /crypt:/ ); for my $char ( split //, $decoded ) { my $encode = chop($key); $encoded .= sprintf( "%.2x", ord($char) ^ ord($encode) ); $key = $encode . $key; } return 'crypt:' . $encoded; } sub EaseeWallbox_decrypt($) { my ($encoded) = @_; my $key = getUniqueId(); my $decoded; return $encoded if ( $encoded !~ /crypt:/ ); $encoded = $1 if ( $encoded =~ /crypt:(.*)/ ); for my $char ( map { pack( 'C', hex($_) ) } ( $encoded =~ /(..)/g ) ) { my $decode = chop($key); $decoded .= chr( ord($char) ^ ord($decode) ); $key = $decode . $key; } return $decoded; } 1; =pod =begin html =end html =cut