diff --git a/fhem/contrib/96_RenaultZE.pm b/fhem/contrib/96_RenaultZE.pm new file mode 100644 index 000000000..6bad623dd --- /dev/null +++ b/fhem/contrib/96_RenaultZE.pm @@ -0,0 +1,1726 @@ +############################################################################### +# +# $Id: 96_RenaultZE.pm 2022-11-16 plin $ +# 96_RenaultZE.pm +# +# Forum : https://forum.fhem.de/index.php/topic,116273.0.html +# +############################################################################### +# +# (c) 2017 Copyright: plin +# All rights reserved +# +# This script 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. +# +# The GNU General Public License can be found at +# http://www.gnu.org/copyleft/gpl.html. +# A copy is found in the textfile GPL.txt and imPORTant notices to the license +# from the author is found in LICENSE.txt distributed with these scripts. +# +# This script 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. +# +################################################################################## + +####################################################################### +# need: +# - HttpUtils +# - Time::Piece +# - JSON +# +######################################################################## + +############################################################################################################################ +# Version History +# v 1.00 added module to the contrib directory +# v 0.32 added attribute disabled +# v 0.31 changed API keys due to change by Renault +# v 0.30 fixed problem with bulk update +# v 0.29 fixed problem with from_json +# v 0.28 fixed timestamp issue +# v 0.27 added error-Reading in case of malformed json string +# v 0.26 fixed decode_json issue (additional tests) +# v 0.25 fixed decode_json issue +# v 0.24 get link for car image from vehicles listing +# v 0.23 pretty print ze_lastErr +# v 0.22 interpret charges data, default time frames for histories +# v 0.21 implemented further get options implemented for Phase 1 already +# v 0.20 implemented zTest attribute to test new options +# v 0.19 fix for time format "2021-01-27T16:41:42+01:00" +# v 0.18 renamed distance to distanceFromHome +# v 0.17 added reverse geocoding +# v 0.16 added distance from home +# v 0.15 minor fix (warning messages) +# v 0.14 detect '' in $data (RenaultZE_gData_Step2) +# v 0.13 fixed timezone problem for UTC timestamps +# v 0.12 fixed attr problem country/county +# v 0.11 fixed parameter problem when using timer +# v 0.10 fixed timer problem +# v 0.9 changed logic, new readings +# v 0.8 suppress 0 readings +# v 0.7 fixed timer problem +# v 0.6 bug fixes +# v 0.5 improved feedback and error code checking +# v 0.4 fix bug when accId = 0 +# v 0.3 adjusted options an placed hint about untested option +# v 0.2 set commands were added +# v 0.1 first version with get options +############################################################################################################################ +# code basis +# - https://github.com/jamesremuscat/pyze +# - https://gist.github.com/mountbatt/772e4512089802a2aa2622058dd1ded7 +# API keys +# - https://renault-wrd-prod-1-euw1-myrapp-one.s3-eu-west-1.amazonaws.com/configuration/android/config_en_DE.json +# KAMEREON_API -> "wiredProd" -> apikey +# GIGYA_API -> "gigyaProd" -> apikey +############################################################################################################################ + +# lock-status + +package main; +use strict; +use warnings; + +use HttpUtils; +use Time::Piece; +#use JSON qw(decode_json); +use JSON; + +my $RenaultZE_version ="V1.00 / 16.11.2022"; + +my %RenaultZE_sets = ( + "AC:on,cancel" => "", + "charge:start,stop" => "", + "password" => "", + "state" => "" +); + +my %RenaultZE_gets = ( + "charge-history" => "", + "charges" => "", + "charging-settings:noArg" => "", + "hvac-history" => "", + "notification-settings:noArg" => "", + "update:noArg" => "", + "vehicles:noArg" => "", + "zTest" => "" +); + +sub RenaultZE_Initialize($) { + my ($hash) = @_; + + $hash->{DefFn} = 'RenaultZE_Define'; + $hash->{UndefFn} = 'RenaultZE_Undef'; + $hash->{SetFn} = 'RenaultZE_Set'; + $hash->{GetFn} = 'RenaultZE_Get'; + $hash->{AttrFn} = 'RenaultZE_Attr'; + $hash->{ReadFn} = 'RenaultZE_Read'; + $hash->{AsyncOutputFn} = 'RenaultZE_AsyncOutput'; + + $hash->{AttrList} = "ze_phase:1,2 ". + "ze_user ". + "ze_country ". + "ze_latitude ". + "ze_longitude ". + "ze_showaddress:0,1 ". + "ze_showimage:0,1,2 ". + "disabled:0,1 ". + $readingFnAttributes; +} + +sub RenaultZE_Define($$) { + my ($hash, $def) = @_; + my @param = split('[ \t]+', $def); + + if(int(@param) < 3) { + return "too few parameters: define RenaultZE "; + } + + my $name = $param[0]; + $hash->{VIN} = $param[2]; + $hash->{INTERVAL} = $param[3]; + + + $hash->{STATE} = "defined"; + $hash->{GIGYA_API} = '3_7PLksOyBRkHv126x5WhHb-5pqC1qFR8pQjxSeLB6nhAnPERTUlwnYoznHSxwX668'; + #$hash->{KAMEREON_API} = 'Ae9FDWugRxZQAGm3Sxgk7uJn6Q4CGEA2'; + $hash->{KAMEREON_API} = 'VAX7XYKGfa92yMvXculCkEFyfZbuM7Ss'; + $hash->{VERSION} = $RenaultZE_version; + + readingsSingleUpdate($hash,"ze_Gigya_JWT_lastCall","0",1) unless (ReadingsVal($name,"ze_Gigya_JWT_lastCall","empty") ne "empty"); + readingsSingleUpdate($hash,"ze_Gigya_JWT_Token","",1) unless (ReadingsVal($name,"ze_Gigya_JWT_Token","empty") ne "empty"); + + $attr{$name}{ze_country} = 'DE' unless (exists($attr{$name}{ze_country})); + $attr{$name}{ze_showaddress} = '1' unless (exists($attr{$name}{ze_showaddress})); + $attr{$name}{ze_showimage} = '1' unless (exists($attr{$name}{ze_showimage})); + + my $firstTrigger = gettimeofday() + 2; + $hash->{TRIGGERTIME} = $firstTrigger; + $hash->{TRIGGERTIME_FMT} = FmtDateTime($firstTrigger); + + RemoveInternalTimer($hash); + InternalTimer($firstTrigger, "RenaultZE_UpdateTimer", $hash, 0); + Log3 $hash, 5, "TRAFFIC: ($name) InternalTimer set to call GetUpdate in 2 seconds for the first time"; + + return undef; +} + +sub RenaultZE_Undef($$) { + my ($hash, $arg) = @_; + # nothing to do + RemoveInternalTimer( $hash ); + return undef; +} + +sub RenaultZE_Get($@) { + my ($hash, @param) = @_; + + my $name = shift @param; + my $opt = shift @param; + my $value = join("", @param); + + if ($opt ne "?") + { + $hash->{FUNCTION} = 'GET'; + $hash->{PARMS} = $opt; + $hash->{PARMVALUE} = $value; + $hash->{curCL} = $hash->{CL}; + } + + Log3 $name, 5, "RenaultZE_Get - opt = $opt, value = $value"; + + readingsSingleUpdate($hash,"ze_lastErr","",1) if ($opt ne "?"); + + if ($opt eq "update") + { + readingsSingleUpdate($hash,"ze_Step","getStatus",1); + RenaultZE_Main1($hash, @param); + } + + elsif ($opt eq "vehicles") + { + readingsSingleUpdate($hash,"ze_Step","getVehicles",1); + RenaultZE_Main1($hash, @param); + } + + elsif ($opt eq "charge-history") + { + if ($value eq "") { + my $tt = localtime()->strftime('%Y%m%d'); + $value = "type=day&start=20000101&end=".$tt; + $hash->{PARMVALUE} = $value; + } + if ( $value =~ /type=month&start=\d{6}&end=\d{6}/ or $value =~ /type=day&start=\d{8}&end=\d{8}/) { + readingsSingleUpdate($hash,"ze_Step","getHistory",1); + RenaultZE_Main1($hash, @param); + } else { + return "Syntax error for $opt, correct pattern is 'type=month&start=202012&end=202101' or 'type=day&start=20201212&end=20210120'"; + } + } + + elsif ($opt eq "charges") + { + if ($value eq "") { + my $tt = localtime()->strftime('%Y%m%d'); + $value = "start=20000101&end=".$tt; + $hash->{PARMVALUE} = $value; + } + if ( $value =~ /start=\d{8}&end=\d{8}/ ) { + readingsSingleUpdate($hash,"ze_Step","getCharges",1); + RenaultZE_Main1($hash, @param); + } else { + return "Syntax error for $opt, correct pattern is 'start=20201212&end=20210120'"; + } + } + + elsif ($opt eq "hvac-history") + { + if ($value eq "") { + my $tt = localtime()->strftime('%Y%m%d'); + $value = "type=day&start=20000101&end=".$tt; + $hash->{PARMVALUE} = $value; + } + if ( $value =~ /type=month&start=\d{6}&end=\d{6}/ or $value =~ /type=day&start=\d{8}&end=\d{8}/) { + readingsSingleUpdate($hash,"ze_Step","getHvacHistory",1); + RenaultZE_Main1($hash, @param); + } else { + return "Syntax error for $opt, correct pattern is 'type=month&start=202012&end=202101' or 'type=day&start=20201212&end=20210120'"; + } + } + + elsif ($opt eq "charging-settings") + { + readingsSingleUpdate($hash,"ze_Step","getChargingSettings",1); + RenaultZE_Main1($hash, @param); + } + + elsif ($opt eq "notification-settings") + { + readingsSingleUpdate($hash,"ze_Step","getNotificationSettings",1); + RenaultZE_Main1($hash, @param); + } + + elsif ($opt eq "zTest") + { + readingsSingleUpdate($hash,"ze_Step","getzTest",1); + RenaultZE_Main1($hash, @param); + } + + elsif($opt eq "?") { + my @cList = keys %RenaultZE_gets; + return "Unknown argument $opt, choose one of " . join(" ", @cList); + } + + return undef; +} + +sub RenaultZE_Set($@) { + my ($hash, @param) = @_; + + my $name = shift @param; + my $opt = shift @param; + my $value = join("", @param); + Log3 $name, 5, "RenaultZE_Set - opt = $opt, value = $value"; + + if ($opt ne "?") + { + $hash->{FUNCTION} = 'SET'; + $hash->{PARMS} = $opt; + $hash->{PARMVALUE} = $value; + $hash->{curCL} = $hash->{CL}; + } + + + if($opt eq "?") { + my @cList = keys %RenaultZE_sets; + return "Unknown argument $opt, choose one of " . join(" ", @cList); + } + + readingsSingleUpdate($hash,"ze_lastErr","",1) if ($opt ne "?"); + + if ($opt eq "AC") + { + readingsSingleUpdate($hash,"ze_Step","setAC",1); + RenaultZE_Main1($hash, @param); + } + + elsif ($opt eq "charge") + { + readingsSingleUpdate($hash,"ze_Step","setCharge",1); + RenaultZE_Main1($hash, @param); + } + + elsif ($opt eq "password" && $value ne "") + { + return RenaultZE_storePassword($name,$value); + } + + elsif ($opt eq "state") + { + $hash->{STATE} = $value; + } + + return undef; +} + +sub RenaultZE_AsyncOutput ($$) +{ + my ( $client_hash, $text ) = @_; + + return $text; +} + +sub RenaultZE_UpdateTimer($) { + my ( $hash ) = @_; + my $name = $hash->{NAME}; + + if(AttrVal($name, "disabled", 0 ) == 1){ + RemoveInternalTimer ($hash); + Log3 $hash, 3, "RenaultZE ($name) is disabled"; + readingsSingleUpdate($hash,"ze_Step","RenaultZE ($name) is disabled",1); + return undef; + } + + if ( $hash->{INTERVAL}) { + RemoveInternalTimer ($hash); + delete($hash->{UPDATESCHEDULE}); + + my $nextTrigger = gettimeofday() + $hash->{INTERVAL}; + $hash->{TRIGGERTIME} = $nextTrigger; + $hash->{TRIGGERTIME_FMT} = FmtDateTime($nextTrigger); + InternalTimer($nextTrigger, "RenaultZE_UpdateTimer", $hash, 0); + Log3 $hash, 4, "RenaultZE ($name) internal interval timer set to call StartUpdate again at " . $hash->{TRIGGERTIME_FMT}; + + readingsSingleUpdate($hash,"ze_Step","getStatus",1); + $hash->{PARMS} = "update"; + $hash->{FUNCTION} = 'GET'; + $hash->{PARMS} = 'update'; + my @param = ('GET', 'update'); + RenaultZE_Main1($hash, @param); + } + +} + +sub RenaultZE_Main1($@) { + my ($hash, @param) = @_; + + #my $name = shift @param; + #my $opt = shift @param; + + my $function = $hash->{FUNCTION}; + my $opt = $hash->{PARMS}; + my $value = $hash->{PARMVALUE}; + my $key = $function."_".$opt; + my $name = $hash->{NAME}; + + Log3 $name, 5, "RenaultZE_Main1 - In, key=".$key; + + #if ($key eq "GET_update" || $key eq "GET_vehicles" || $key eq "GET_ac-state" || $key eq "SET_AC" || $key eq "SET_charge") + #{ + readingsSingleUpdate($hash,"ze_Step","Main1",1); + my $lastErr = $hash->{READINGS}{ze_lastErr}{VAL}; + readingsSingleUpdate($hash,"ze_Gigya_JWT_Token","",1) if ($lastErr ne ""); + my $ze_Gigya_JWT_Token = $hash->{READINGS}{ze_Gigya_JWT_Token}{VAL}; + my $ze_Gigya_JWT_lastCall = $hash->{READINGS}{ze_Gigya_JWT_lastCall}{TIME}; + my $res = 0; + + Log3 $name, 5, "RenaultZE_Main1 - ze_Gigya_JWT_lastCall=".$ze_Gigya_JWT_lastCall; + my $gigya_time = Time::Piece->strptime( $ze_Gigya_JWT_lastCall, '%Y-%m-%d %H:%M:%S')->epoch; + Log3 $name, 5, "RenaultZE_Main1 - ze_Gigya_JWT_lastCall=".$gigya_time; + Log3 $name, 5, "RenaultZE_Main1 - gettimeofday=".gettimeofday(); + + if ( $ze_Gigya_JWT_Token eq "" || $gigya_time < gettimeofday() - 70000 ) { + $res = RenaultZE_getCreds_Step1($hash); + Log3 $name, 5, "RenaultZE_Main1 - RC=".$res; + } + else + { + Log3 $name, 5, "RenaultZE_Main1 - ze_Gigya_JWT_Token=>".$res."<"; + } + RenaultZE_Main2($hash); + return undef; + #} + + Log3 $name, 5, "RenaultZE_Main1 - Out"; + return undef; +} + +sub RenaultZE_Main2($) { + my ($hash) = @_; + + my $function = $hash->{FUNCTION}; + my $opt = $hash->{PARMS}; + my $value = $hash->{PARMVALUE}; + my $key = $function."_".$opt; + my $name = $hash->{NAME}; + + Log3 $name, 5, "RenaultZE_Main2 - In, key=".$key; + + my $lastErr = $hash->{READINGS}{ze_lastErr}{VAL}; + return undef if ($lastErr ne ""); + + #if ($key eq "GET_update" || $key eq "GET_vehicles" || $key eq "GET_ac-state" || $key eq "SET_AC" || $key eq "SET_charge") + #{ + readingsSingleUpdate($hash,"ze_Step","Main2",1); + my $ze_Renault_AccId = $hash->{READINGS}{ze_Renault_AccId}{VAL}; + my $res = 0; + + Log3 $name, 5, "RenaultZE_Main2 - ze_Renault_AccId: ".$ze_Renault_AccId; + if ( $ze_Renault_AccId eq "" || $ze_Renault_AccId eq "0" ){ + $res = RenaultZE_getAccId_Step1($hash); + Log3 $name, 5, "RenaultZE_getAccId_Step1 - RC=".$res; + } + RenaultZE_Main3($hash); + return undef; + #} + + Log3 $name, 5, "RenaultZE_Main2 - Out"; + return undef; +} + +sub RenaultZE_Main3($) { + my ($hash) = @_; + + my $function = $hash->{FUNCTION}; + my $opt = $hash->{PARMS}; + my $value = $hash->{PARMVALUE}; + my $key = $function."_".$opt; + my $name = $hash->{NAME}; + + readingsSingleUpdate($hash,"ze_Step","Main3",1); + Log3 $name, 5, "RenaultZE_Main3 - In, key=".$key; + + my $lastErr = $hash->{READINGS}{ze_lastErr}{VAL}; + return undef if ($lastErr ne ""); + + my $phase = AttrVal($name,"ze_phase",""); + + if ($key eq "GET_update") + { + #my $res = RenaultZE_getData_Step1($hash); + my $res = RenaultZE_gData_Step1($hash,'battery-status'); + Log3 $name, 5, "RenaultZE_gData_Step1 - battery-status - RC=".$res; + $res = RenaultZE_gData_Step1($hash,'cockpit'); + Log3 $name, 5, "RenaultZE_gData_Step1 - cockpit - RC=".$res; + $res = RenaultZE_gData_Step1($hash,'location') if ($phase eq "2"); + Log3 $name, 5, "RenaultZE_gData_Step1 - location - RC=".$res if ($phase eq "2"); + $res = RenaultZE_gData_Step1($hash,'hvac-status') if ($phase eq "1"); + Log3 $name, 5, "RenaultZE_gData_Step1 - hvac-status - RC=".$res if ($phase eq "1"); + $res = RenaultZE_gData_Step1($hash,'charge-mode'); + Log3 $name, 5, "RenaultZE_gData_Step1 - charge-mode - RC=".$res; + } + + if ($key eq "GET_vehicles") + { + my $res = RenaultZE_gData_Step1($hash,'vehicles'); + Log3 $name, 5, "RenaultZE_gData_Step1 - vehicles - RC=".$res; + } + + if ($key eq "GET_charge-history") + { + my $res = RenaultZE_gData_Step1($hash,'charge-history'); + Log3 $name, 5, "RenaultZE_gData_Step1 - charge-history - RC=".$res; + } + + if ($key eq "GET_charges") + { + my $res = RenaultZE_gData_Step1($hash,'charges'); + Log3 $name, 5, "RenaultZE_gData_Step1 - charges - RC=".$res; + } + + if ($key eq "GET_charging-settings") + { + my $res = RenaultZE_gData_Step1($hash,'charging-settings'); + Log3 $name, 5, "RenaultZE_gData_Step1 - charging-settings - RC=".$res; + } + + if ($key eq "GET_hvac-history") + { + my $res = RenaultZE_gData_Step1($hash,'hvac-history'); + Log3 $name, 5, "RenaultZE_gData_Step1 - hvac-history - RC=".$res; + } + + if ($key eq "GET_notification-settings") + { + my $res = RenaultZE_gData_Step1($hash,'notification-settings'); + Log3 $name, 5, "RenaultZE_gData_Step1 - notification-settings - RC=".$res; + } + + if ($key eq "GET_zTest") + { + my $res = RenaultZE_gData_Step1($hash,'zTest'); + Log3 $name, 5, "RenaultZE_gData_Step1 - zTest - RC=".$res; + } + + if ($key eq "SET_AC") + { + my $res = RenaultZE_AC_Step1($hash); + Log3 $name, 5, "RenaultZE_AC_Step1 - RC=".$res; + } + + if ($key eq "SET_charge") + { + my $res = RenaultZE_Charge_Step1($hash); + Log3 $name, 5, "RenaultZE_Charge_Step1 - RC=".$res; + } + + Log3 $name, 5, "RenaultZE_Main3 - Out"; +} + +sub RenaultZE_Main4($) { + my ($hash) = @_; + my $name = $hash->{NAME}; + + my $lastErr = $hash->{READINGS}{ze_lastErr}{VAL}; + return undef if ($lastErr ne ""); + + readingsSingleUpdate($hash,"ze_Step","done",1); + $hash->{STATE} = "updated"; + return undef; +} + +sub RenaultZE_Attr(@) { + my ($cmd,$name,$attrName,$attrVal) = @_; + my $hash = $defs{$name}; + if($cmd eq "set") { + if (substr($attrName ,0,4) eq "ze_") + { + $_[3] = $attrVal; + $hash->{".reset"} = 1 if defined($hash->{LPID}); + } + elsif (($attrName eq "disabled") && ($attrVal == 1)) + { + readingsSingleUpdate($hash,"state","disabled",1); + readingsSingleUpdate($hash,"ze_Step","RenaultZE ($name) is disabled",1); + $_[3] = $attrVal; + $hash->{".reset"} = 1 if defined($hash->{LPID}); + RemoveInternalTimer ($hash); + } + elsif (($attrName eq "disabled") && ($attrVal == 0)) + { + readingsSingleUpdate($hash,"ze_Step","RenaultZE ($name) is enabled",1); + $_[3] = $attrVal; + $hash->{".reset"} = 1 if defined($hash->{LPID}); + my $firstTrigger = gettimeofday() + 2; + $hash->{TRIGGERTIME} = $firstTrigger; + $hash->{TRIGGERTIME_FMT} = FmtDateTime($firstTrigger); + InternalTimer($firstTrigger, "RenaultZE_UpdateTimer", $hash, 0); + } + } + elsif ($cmd eq "del") + { + if (substr($attrName,0,3) eq "ze_") + { + $_[3] = $attrVal; + $hash->{".reset"} = 1 if defined($hash->{LPID}); + } + elsif (($attrName eq "disabled") ) + { + readingsSingleUpdate($hash,"state","denbled",1); + readingsSingleUpdate($hash,"ze_Step","RenaultZE ($name) is enabled",1); + $_[3] = $attrVal; + $hash->{".reset"} = 1 if defined($hash->{LPID}); + my $firstTrigger = gettimeofday() + 2; + $hash->{TRIGGERTIME} = $firstTrigger; + $hash->{TRIGGERTIME_FMT} = FmtDateTime($firstTrigger); + InternalTimer($firstTrigger, "RenaultZE_UpdateTimer", $hash, 0); + } + } + + return undef; +} + +###################################################### +# storePW & readPW Code geklaut aus 72_FRITZBOX.pm :) +###################################################### +sub RenaultZE_storePassword($$) +{ + my ($name, $password) = @_; + my $index = "ZE_".$name."_passwd"; + my $key = getUniqueId().$index; + my $e_pwd = ""; + + if (eval "use Digest::MD5;1") + { + $key = Digest::MD5::md5_hex(unpack "H*", $key); + $key .= Digest::MD5::md5_hex($key); + } + + for my $char (split //, $password) + { + my $encode=chop($key); + $e_pwd.=sprintf("%.2x",ord($char)^ord($encode)); + $key=$encode.$key; + } + + my $error = setKeyValue($index, $e_pwd); + return "error while saving ZE user password : $error" if(defined($error)); + return "ZE user password successfully saved in FhemUtils/uniqueID Key $index"; +} + +sub RenaultZE_readPassword($) +{ + my ($name) = @_; + my $index = "ZE_".$name."_passwd"; + my $key = getUniqueId().$index; + + my ($password, $error); + + #Log3 $name,5,"$name, read ZE user password from FhemUtils/uniqueID Key $key"; + ($error, $password) = getKeyValue($index); + + if ( defined($error) ) + { + Log3 $name,5, "$name, cant't read ZE user password from FhemUtils/uniqueID: $error"; + return undef; + } + + if ( defined($password) ) + { + if (eval "use Digest::MD5;1") + { + $key = Digest::MD5::md5_hex(unpack "H*", $key); + $key .= Digest::MD5::md5_hex($key); + } + + my $dec_pwd = ''; + + for my $char (map { pack('C', hex($_)) } ($password =~ /(..)/g)) + { + my $decode=chop($key); + $dec_pwd.=chr(ord($char)^ord($decode)); + $key=$decode.$key; + } + return $dec_pwd; + } + else + { + Log3 $name,3,"$name, no ZE user password found in FhemUtils/uniqueID"; + return undef; + } +} + +####### getStatus Dialog ##### +# +sub RenaultZE_getCreds_Step1($) +{ + my ($hash) = @_; + my $name = $hash->{NAME}; + Log3 $name, 5, "RenaultZE_getCreds_Step1 - In ".$hash."/".$name; + readingsSingleUpdate($hash,"ze_Step","RenaultZE_getCreds_Step1",1); + my $gigya_api = $hash->{GIGYA_API}; + my $username = AttrVal($name,"ze_user",""); + my $password = RenaultZE_readPassword($name); + Log3 $name, 5, "RenaultZE_getCreds_Step1 - Parms: ".$gigya_api."/".$username."/".$password; + + my $step1= { + ApiKey => $gigya_api, + loginId => $username, + password => $password, + include => 'data', + sessionExpiration => 60 + }; + + Log3 $name, 5, "RenaultZE_getCreds_Step1 - Data".$step1; + my $param = { + url => "https://accounts.eu1.gigya.com/accounts.login", + header => "Content-type: application/x-www-form-urlencoded", + hash => $hash, + timeout => 15, + method => "POST", + data => $step1, + callback => \&RenaultZE_getCreds_Step2 + }; + + HttpUtils_NonblockingGet($param); # Starten der HTTP Abfrage. Es gibt keinen Return-Code. + Log3 $name, 5, "RenaultZE_getCreds_Step1 - Out"; + return undef; +} + +sub RenaultZE_getCreds_Step2($) +{ + my ($param, $err, $data) = @_; + my $hash = $param->{hash}; + my $name = $hash->{NAME}; + + Log3 $name, 5, "RenaultZE_getCreds_Step2 - In ".$hash."/".$name; + readingsSingleUpdate($hash,"ze_Step","RenaultZE_getCreds_Step2",1); + + RenaultZE_Error_err($hash,"RenaultZE_getCreds_Step2",$param->{url},$err,$data) if($err ne ""); + RenaultZE_Log_Data($hash,"RenaultZE_getCreds_Step2",$param->{url},$err,$data) if($data ne ""); + return undef if (RenaultZE_CheckJson($hash,$data)); + my $decode_json = from_json($data); + my $errorCode = $decode_json->{errorCode}; + RenaultZE_Error_errorCode1($hash,"RenaultZE_getCreds_Step2",$param->{url},$err,$data) if($errorCode ne 0); + + my $lastErr = $hash->{READINGS}{ze_lastErr}{VAL}; + return undef if ($lastErr ne ""); + + $decode_json = from_json($data); + my $ze_personId = $decode_json->{data}->{personId}; + my $oauth_token = $decode_json->{sessionInfo}->{cookieValue}; + Log3 $name, 5, "RenaultZE_getCreds_Step2 - ze_personId:".$ze_personId.", ze_cookieValue:".$oauth_token; + readingsBeginUpdate($hash); + readingsBulkUpdate($hash,"ze_personId",$ze_personId); + readingsBulkUpdate($hash,"ze_cookieValue",$oauth_token); + readingsEndUpdate($hash, 1 ); + + my $gigya_api = $hash->{GIGYA_API}; + my $step2= { + login_token => $oauth_token, + ApiKey => $gigya_api, + fields => 'data.personId,data.gigyaDataCenter', + expiration => 87000 + }; + Log3 $name, 5, "RenaultZE_getCreds_Step2 - Data".$step2; + my $param2 = { + url => "https://accounts.eu1.gigya.com/accounts.getJWT", + header => "Content-type: application/x-www-form-urlencoded", + hash => $hash, + timeout => 15, + method => "POST", + data => $step2, + callback => \&RenaultZE_getCreds_Step3 + }; + + HttpUtils_NonblockingGet($param2); # Starten der HTTP Abfrage. Es gibt keinen Return-Code. + #my $res = RenaultZE_getStatusPerformHttpRequest2($i, $e, $o, $a); + Log3 $name, 5, "RenaultZE_getCreds_Step2 - Out"; + return undef; +} + +sub RenaultZE_getCreds_Step3($) +{ + my ($param, $err, $data) = @_; + my $hash = $param->{hash}; + my $name = $hash->{NAME}; + + Log3 $name, 5, "RenaultZE_getCreds_Step3 - In ".$hash."/".$name; + readingsSingleUpdate($hash,"ze_Step","RenaultZE_getCreds_Step3",1); + + RenaultZE_Error_err($hash,"RenaultZE_getCreds_Step3",$param->{url},$err,$data) if($err ne ""); + RenaultZE_Log_Data($hash,"RenaultZE_getCreds_Step3",$param->{url},$err,$data) if($data ne ""); + return undef if (RenaultZE_CheckJson($hash,$data)); + my $decode_json = from_json($data); + my $errorCode = $decode_json->{errorCode}; + RenaultZE_Error_errorCode1($hash,"RenaultZE_getCreds_Step3",$param->{url},$err,$data) if($errorCode ne 0); + + my $lastErr = $hash->{READINGS}{ze_lastErr}{VAL}; + return undef if ($lastErr ne ""); + + $decode_json = from_json($data); + my $id_token = $decode_json->{id_token}; + readingsBeginUpdate($hash); + readingsBulkUpdate($hash,"ze_Gigya_JWT_Token",$id_token); + readingsBulkUpdate($hash,"ze_Gigya_JWT_lastCall",localtime(time)); + readingsEndUpdate($hash, 1 ); + + RenaultZE_Main2($hash); + + #my $res = RenaultZE_getStatusPerformHttpRequest2($i, $e, $o, $a); + Log3 $name, 5, "RenaultZE_getCreds_Step3 - Out"; +} + +sub RenaultZE_getAccId_Step1($) +{ + my ($hash) = @_; + my $name = $hash->{NAME}; + Log3 $name, 5, "RenaultZE_getAccId_Step1 - In ".$hash."/".$name; + readingsSingleUpdate($hash,"ze_Step","RenaultZE_getAccId_Step1",1); + my $kamereon_api = $hash->{KAMEREON_API}; + my $id_token = $hash->{READINGS}{ze_Gigya_JWT_Token}{VAL}; + my $ze_personId = $hash->{READINGS}{ze_personId}{VAL}; + my $country = AttrVal($name,"ze_country","DE"); + Log3 $name, 5, "RenaultZE_getCreds_Step1 - Parms: ".$kamereon_api."/".$id_token; + + return undef if ( $id_token eq "" || $ze_personId eq "" ); + + my $step1= { + 'ApiKey' => $kamereon_api, + 'x-gigya-id_token' => $id_token + }; + + Log3 $name, 5, "RenaultZE_getCreds_Step1 - Data".$step1; + my $url = "https://api-wired-prod-1-euw1.wrd-aws.com/commerce/v1/persons/".$ze_personId."?country=".$country; + Log3 $name, 5, "RenaultZE_getCreds_Step1 - URL ".$url; + my $param = { + url => $url, + header => $step1, + hash => $hash, + timeout => 15, + method => "GET", + callback => \&RenaultZE_getAccId_Step2 + }; + + HttpUtils_NonblockingGet($param); + Log3 $name, 5, "RenaultZE_getAccId_Step1 - Out"; + return undef; +} + +sub RenaultZE_getAccId_Step2($) +{ + my ($param, $err, $data) = @_; + my $hash = $param->{hash}; + my $name = $hash->{NAME}; + + Log3 $name, 5, "RenaultZE_getAccId_Step2 - In ".$hash."/".$name; + readingsSingleUpdate($hash,"ze_Step","RenaultZE_getAccId_Step2",1); + + RenaultZE_Error_err($hash,"RenaultZE_getAccId_Step2",$param->{url},$err,$data) if($err ne ""); + RenaultZE_Log_Data($hash,"RenaultZE_getAccId_Step2",$param->{url},$err,$data) if($data ne ""); + RenaultZE_Error_errorCode2($hash,"RenaultZE_getAccId_Step2",$param->{url},$err,$data) if($data =~ /error/); + + my $lastErr = $hash->{READINGS}{ze_lastErr}{VAL}; + return undef if ($lastErr ne ""); + + return undef if (RenaultZE_CheckJson($hash,$data)); + my $decode_json = from_json($data); + my $accountId = $decode_json->{accounts}[0]->{accountId}; + Log3 $name, 5, "RenaultZE_getCreds_Step2 - accountId:".$accountId; + readingsSingleUpdate($hash,"ze_Renault_AccId",$accountId,1); + + RenaultZE_Main3($hash); + + Log3 $name, 5, "RenaultZE_getCAccId_Step2 - Out"; +} + +sub RenaultZE_gData_Step1($$) +{ + my ($hash,$tree) = @_; + my $name = $hash->{NAME}; + + my $v1v2 = "v1"; + $v1v2 = "v2" if ( $tree eq "battery-status"); + my $shortlong = "long"; + $shortlong = "short" if ( $tree eq "vehicles"); + my $popup = "no"; + $popup = "yes" if ( $tree eq "vehicles"); + $popup = "yes" if ( $tree eq "charge-mode"); + my $testparms = $hash->{PARMVALUE}; + my $timespecs = ""; + if ( $tree eq "charge-history" or $tree eq "hvac-history" or $tree eq "charges") { + $timespecs = "&".$testparms; + } + + Log3 $name, 5, "RenaultZE_gData_Step1 - In ".$hash."/".$tree."/".$name; + readingsSingleUpdate($hash,"ze_Step","RenaultZE_gData_Step1",1); + my $kamereon_api = $hash->{KAMEREON_API}; + my $id_token = $hash->{READINGS}{ze_Gigya_JWT_Token}{VAL}; + my $accId = $hash->{READINGS}{ze_Renault_AccId}{VAL}; + my $vin = $hash->{VIN}; + my $country = AttrVal($name,"ze_country","DE"); + Log3 $name, 5, "RenaultZE_gData_Step1 - Parms: ".$kamereon_api."/".$id_token; + + return 4 if ( $id_token eq "" || $accId eq "" ); + my $header= { + 'apikey' => $kamereon_api, + 'x-gigya-id_token' => $id_token + }; + + Log3 $name, 5, "RenaultZE_getData_Step1 - Data".$header; + my $url = ""; + if ( $shortlong eq "long") { + $url = "https://api-wired-prod-1-euw1.wrd-aws.com/commerce/v1/accounts/".$accId."/kamereon/kca/car-adapter/".$v1v2."/cars/".$vin."/".$tree."?country=".$country.$timespecs; + }else { + $url = "https://api-wired-prod-1-euw1.wrd-aws.com/commerce/v1/accounts/".$accId."/".$tree."?country=".$country; + } + + # for development of new options and users of a phase 1 Zoe + if ( $tree eq "zTest") { + my $testparms = $hash->{PARMVALUE}; + $url = "https://api-wired-prod-1-euw1.wrd-aws.com/commerce/v1/accounts/".$accId."/kamereon/kca/car-adapter/".$v1v2."/cars/".$vin."/".$testparms; + } + # In Vorbereit8ung, aber derzeit noch nicht seitens Renault unterstützt: + #$url = "https://api-wired-prod-1-euw1.wrd-aws.com/commerce/v1/accounts/".$accId."/kamereon/kca/car-adapter/".$v1v2."/cars/".$vin."/hvac-history?type=day&start=20201101&end=20210108&country=".$country; + #$url = "https://api-wired-prod-1-euw1.wrd-aws.com/commerce/v1/accounts/".$accId."/kamereon/kca/car-adapter/".$v1v2."/cars/".$vin."/hvac-sessions?start=20201101&end=20210108&country=".$country; + #$url = "https://api-wired-prod-1-euw1.wrd-aws.com/commerce/v1/accounts/".$accId."/kamereon/kca/car-adapter/".$v1v2."/cars/".$vin."/charges?start=20201101&end=20210108&country=".$country; + #$url = "https://api-wired-prod-1-euw1.wrd-aws.com/commerce/v1/accounts/".$accId."/kamereon/kca/car-adapter/".$v1v2."/cars/".$vin."/charge-history?type=day&start=20201101&end=20210108&country=".$country; + #$url = "https://api-wired-prod-1-euw1.wrd-aws.com/commerce/v1/accounts/".$accId."/kamereon/kca/car-adapter/".$v1v2."/cars/".$vin."/charge-history?type=month&start=202011&end=202101&country=".$country; + #$url = "https://api-wired-prod-1-euw1.wrd-aws.com/commerce/v1/accounts/".$accId."/kamereon/kca/car-adapter/".$v1v2."/cars/".$vin."/lock-status?country=".$country; + Log3 $name, 5, "RenaultZE_gData_Step1 - URL ".$url; + my $param = { + url => $url, + header => $header, + hash => $hash, + timeout => 15, + method => "GET", + callback => \&RenaultZE_gData_Step2 + }; + + HttpUtils_NonblockingGet($param); # Starten der HTTP Abfrage. Es gibt keinen Return-Code. + Log3 $name, 5, "RenaultZE_gData_Step1 - Out"; + return 0; +} + +sub RenaultZE_gData_Step2($) +{ + my ($param, $err, $data) = @_; + my $hash = $param->{hash}; + my $name = $hash->{NAME}; + + Log3 $name, 5, "RenaultZE_gData_Step2 - In ".$hash."/".$name; + readingsSingleUpdate($hash,"ze_Step","RenaultZE_gData_Step2",1); + + RenaultZE_Error_err($hash,"RenaultZE_gData_Step2",$param->{url},$err,$data) if($err ne ""); + RenaultZE_Log_Data($hash,"RenaultZE_gData_Step2",$param->{url},$err,$data) if($data ne ""); + RenaultZE_Error_errorCode2($hash,"RenaultZE_gData_Step2",$param->{url},$err,$data) if($data =~ /errorMessage/); + + my $lastErr = $hash->{READINGS}{ze_lastErr}{VAL}; + return undef if ($lastErr ne ""); + + Log3 $name, 3, "RenaultZE_gData_Step2 - DataError ".$data if ($data =~ /\/); + return undef if ($data =~ /\/); + + my $phase = AttrVal($name,"ze_phase",""); + + return undef if (RenaultZE_CheckJson($hash,$data)); + my $decode_json = from_json($data); + + if($data =~ /batteryLevel/) { + my $timestamp = $decode_json->{data}->{attributes}->{timestamp}; + #$timestamp =~ s/\+01:00/Z/sg; # fix for time format "2021-01-27T16:41:42+01:00" + #my $t = Time::Piece->strptime($timestamp, "%Y-%m-%dT%H:%M:%SZ")->epoch; + my $t = RenaultZE_EpochFromDateTime($timestamp); + my $tt = localtime($t)->strftime('%Y-%m-%d %H:%M:%S'); + readingsBeginUpdate($hash); + readingsBulkUpdate($hash,"timestamp",$tt); + readingsBulkUpdate($hash,"batteryLevel",$decode_json->{data}->{attributes}->{batteryLevel}); + readingsBulkUpdate($hash,"batteryTemperature",$decode_json->{data}->{attributes}->{batteryTemperature}) if ($phase eq "1"); + readingsBulkUpdate($hash,"batteryAutonomy",$decode_json->{data}->{attributes}->{batteryAutonomy}); + readingsBulkUpdate($hash,"batteryCapacity",$decode_json->{data}->{attributes}->{batteryCapacity}) if ($decode_json->{data}->{attributes}->{batteryCapacity} gt 0); + readingsBulkUpdate($hash,"batteryAvailableEnergy",$decode_json->{data}->{attributes}->{batteryAvailableEnergy}) if ($decode_json->{data}->{attributes}->{batteryAvailableEnergy} gt 0); + readingsBulkUpdate($hash,"plugStatus",$decode_json->{data}->{attributes}->{plugStatus}); + readingsBulkUpdate($hash,"chargingStatus",$decode_json->{data}->{attributes}->{chargingStatus}); + readingsBulkUpdate($hash,"chargingRemainingTime",$decode_json->{data}->{attributes}->{chargingRemainingTime}); + readingsBulkUpdate($hash,"chargingInstantaneousPower",$decode_json->{data}->{attributes}->{chargingInstantaneousPower}); + readingsEndUpdate($hash, 1 ); + return 0; + } + + if($data =~ /totalMileage/) { + readingsSingleUpdate($hash,"totalMileageKm",$decode_json->{data}->{attributes}->{totalMileage},1); + return 0; + } + + my $gpsLatitude = ""; + my $gpsLongitude = ""; + my $lastUpdateTime = ""; + if($data =~ /gpsLatitude/) { + if ($data =~ /locationStatus/) { + $gpsLatitude = $decode_json->{locationStatus}->{attributes}->{gpsLatitude}; + $gpsLongitude = $decode_json->{locationStatus}->{attributes}->{gpsLongitude}; + $lastUpdateTime = $decode_json->{locationStatus}->{attributes}->{lastUpdateTime}; + } else { + $gpsLatitude = $decode_json->{data}->{attributes}->{gpsLatitude}; + $gpsLongitude = $decode_json->{data}->{attributes}->{gpsLongitude}; + $lastUpdateTime = $decode_json->{data}->{attributes}->{lastUpdateTime}; + } + my $gpsLatitude = $decode_json->{data}->{attributes}->{gpsLatitude}; + my $gpsLongitude = $decode_json->{data}->{attributes}->{gpsLongitude}; + my $lastUpdateTime = $decode_json->{data}->{attributes}->{lastUpdateTime}; + #$lastUpdateTime =~ s/\+01:00/Z/sg; # fix for time format "2021-01-27T16:41:42+01:00" + #my $t = Time::Piece->strptime($lastUpdateTime, "%Y-%m-%dT%H:%M:%SZ")->epoch; + my $t = RenaultZE_EpochFromDateTime($lastUpdateTime); + my $tt = localtime($t)->strftime('%Y-%m-%d %H:%M:%S'); + my $oldlat = ReadingsVal($name,"gpsLatitude","empty"); + my $oldlong = ReadingsVal($name,"gpsLongitude","empty"); + my $link = "Google Maps"; + if ( $oldlat != $gpsLatitude or $oldlong != $gpsLongitude ) { + Log3 $name, 5, "RenaultZE_gData_Step2 - GPS ".$oldlat."/".$gpsLatitude." ".$oldlong."/".$gpsLongitude; + RenaultZE_distanceFromHome($hash,$gpsLatitude,$gpsLongitude); + } + readingsBeginUpdate($hash); + readingsBulkUpdate($hash,"gpsLatitude",$gpsLatitude); + readingsBulkUpdate($hash,"gpsLongitude",$gpsLongitude); + readingsBulkUpdate($hash,"gpsLastUpdateTime",$tt); + readingsBulkUpdate($hash,"gpsGoogleMaps",$link); + readingsEndUpdate($hash, 1 ); + return 0; + } + + if($data =~ /vehicleLink/) { + my $decode_json = from_json($data); + my $output = JSON->new->ascii->pretty->encode(decode_json join '', $data); + + # extract image urls + my $mtab = $decode_json->{vehicleLinks}; + foreach my $item( @$mtab ) { + next if ($item->{vin} ne $hash->{VIN}); + my $assets = $item->{vehicleDetails}->{assets}; + foreach my $ass( @$assets ) { + my $cars = $ass->{renditions}; + foreach my $car( @$cars ) { + my $url = $car->{url}; + my $size = $car->{resolutionType}; + my $link = ""; + readingsSingleUpdate($hash,"img_".$size."_url",$url,1); + readingsSingleUpdate($hash,"img_".$size."_img",$link,1) if (AttrVal($name,"ze_showimage","1") gt 0 and $size =~ /SMALL/ ); + readingsSingleUpdate($hash,"img_".$size."_img",$link,1) if (AttrVal($name,"ze_showimage","1") eq 2 and $size =~ /LARGE/ ); + } + } + } + + asyncOutput( $hash->{curCL}, $output ); + return 0; + } + + if($data =~ /externalTemperature/) { + my $decode_json = from_json($data); + readingsSingleUpdate($hash,"externalTemperature",$decode_json->{data}->{attributes}->{externalTemperature},1); + return 0; + } + + if($data =~ /chargeMode/) { + my $decode_json = from_json($data); + readingsSingleUpdate($hash,"chargeMode",$decode_json->{data}->{attributes}->{chargeMode},1); + return 0; + } + + ### charge-history?country=DE&type=day&start=20201212&end=20210120 + ### charge-history?country=DE&type=month&start=202012&end=202101 + if($data =~ /chargeSummaries.*totalChargesNumber/) { + my $mtab = $decode_json->{data}->{attributes}->{chargeSummaries}; + my $tperiod = "day"; + $tperiod = "month" if ($data =~ /month/); + print ">>>".$tperiod."\n"; + my $output = "Charge Summaries (".$tperiod.")"; + $output = $output.""; + $output = $output.""; + $output = $output.""; + $output = $output.""; + $output = $output.""; + $output = $output.""; + foreach my $item( @$mtab ) { + $output = $output.""; + $output = $output.""; + $output = $output.""; + $output = $output.""; + $output = $output.""; + $output = $output.""; + } + $output = $output."
".$tperiod."totalChargesNumbertotalChargesDurationtotalChargesErrors
".$item->{$tperiod}."".$item->{totalChargesNumber}."".$item->{totalChargesDuration}."".$item->{totalChargesErrors}."
"; + readingsSingleUpdate($hash,"chargeHistory",$output,1); + return 0; + } + + ### hvac-history?country=DE&type=month&start=202012&end=202101 + ### hvac-history?country=DE&type=day&start=20201212&end=20210120 + if($data =~ /hvacSessionsSummaries.*totalHvacSessionsNumber/) { + my $mtab = $decode_json->{data}->{attributes}->{hvacSessionsSummaries}; + my $tperiod = "day"; + $tperiod = "month" if ($data =~ /month/); + print ">>>".$tperiod."\n"; + my $output = "HVAC Session Summaries (".$tperiod.")"; + $output = $output.""; + $output = $output.""; + $output = $output.""; + $output = $output.""; + $output = $output.""; + foreach my $item( @$mtab ) { + $output = $output.""; + $output = $output.""; + $output = $output.""; + $output = $output.""; + $output = $output.""; + } + $output = $output."
".$tperiod."totalHvacSessionsNumbertotalHvacSessionsErrors
".$item->{$tperiod}."".$item->{totalHvacSessionsNumber}."".$item->{totalHvacSessionsErrors}."
"; + readingsSingleUpdate($hash,"hvacHistory",$output,1); + return 0; + } + + ### hvac-sessions?country=DE&start=20201210&end=20210110 + if($data =~ /hvacSessions.*hvacSessionRequestDate/) { + my $mtab = $decode_json->{data}->{attributes}->{hvacSessions}; + #print scalar @$mtab."\n"; + my $output = "HVAC Sessions"; + $output = $output.""; + $output = $output.""; + $output = $output.""; + $output = $output.""; + $output = $output.""; + foreach my $item( @$mtab ) { + $output = $output.""; + $output = $output.""; + $output = $output.""; + $output = $output.""; + $output = $output.""; + } + $output = $output."
hvacSessionRequestDatehvacSessionStartDatehvacSessionEndStatus
".$item->{hvacSessionRequestDate}."".$item->{hvacSessionStartDate}."".$item->{hvacSessionEndStatus}."
"; + readingsSingleUpdate($hash,"hvacSessions",$output,1); + return 0; + } + + ### charging-settings?country=DE + if($data =~ /mode.*schedules/) { + my $mtab = $decode_json->{data}->{attributes}->{schedules}; + #print scalar @$mtab."\n"; + my $output = "Charging Settings

Mode=".$decode_json->{data}->{attributes}->{mode}."
"; + my @wdays = ("monday", "tuesday", "wednesday", "thursday", "friday", "saturday", "saturday" ); + foreach my $item( @$mtab ) { + $output = $output."Schedules, activated =".$item->{activated}.""; + $output = $output.""; + $output = $output.""; + $output = $output.""; + $output = $output.""; + $output = $output.""; + foreach my $wd( @wdays ) { + $output = $output.""; + $output = $output.""; + $output = $output.""; + $output = $output.""; + $output = $output.""; + } + last; + } + $output = $output."
Day of WeekstartTimeduration
".$wd."".$item->{$wd}->{startTime}."".$item->{$wd}->{duration}."
"; + readingsSingleUpdate($hash,"chargingSettings",$output,1); + return 0; + } + + ### notification-settings?country=DE + if($data =~ /settings.*messageKey/) { + my $mtab = $decode_json->{data}->{attributes}->{settings}; + my $output = "Notification Settings"; + $output = $output.""; + $output = $output.""; + $output = $output.""; + $output = $output.""; + $output = $output.""; + $output = $output.""; + foreach my $item( @$mtab ) { + $output = $output.""; + $output = $output.""; + $output = $output.""; + $output = $output.""; + $output = $output.""; + $output = $output.""; + } + $output = $output."
messageKeyemailsmspushApp
".$item->{messageKey}."".$item->{email}."".$item->{sms}."".$item->{pushApp}."
"; + readingsSingleUpdate($hash,"notificationSettings",$output,1); + return 0; + } + + ### charges start=20200202&end=20210202 + if($data =~ /charges/) { + my $mtab = $decode_json->{data}->{attributes}->{charges}; + my $output = "Charges"; + $output = $output.""; + $output = $output.""; + $output = $output.""; + $output = $output.""; + $output = $output.""; + $output = $output.""; + $output = $output.""; + $output = $output.""; + $output = $output.""; + $output = $output.""; + foreach my $item( @$mtab ) { + $output = $output.""; + $output = $output.""; + $output = $output.""; + $output = $output.""; + $output = $output.""; + $output = $output.""; + $output = $output.""; + $output = $output.""; + $output = $output.""; + $output = $output.""; + } + $output = $output."
chargeStartDatechargeEndDatechargeDurationchargeStartBatteryLevelchargeBatteryLevelRecoveredchargePowerchargeStartInstantaneousPowerchargeEndStatus
".$item->{chargeStartDate}."".$item->{chargeEndDate}."".$item->{chargeDuration}."".$item->{chargeStartBatteryLevel}."".$item->{chargeBatteryLevelRecovered}."".$item->{chargePower}."".$item->{chargeStartInstantaneousPower}."".$item->{chargeEndStatus}."
"; + $output =~ s/charge//g; + readingsSingleUpdate($hash,"chargesDetails",$output,1); + return 0; + } + + Log3 $name, 5, "RenaultZE_gData_Step2 - opt=".$hash->{PARMS}; + if($hash->{PARMS} eq "zTest") { + asyncOutput( $hash->{curCL}, $data ); + return 0; + } + + Log3 $name, 5, "RenaultZE_gData_Step2 - Out"; + return 0; +} + +sub RenaultZE_AC_Step1($) +{ + my ($hash) = @_; + my $name = $hash->{NAME}; + my $value = $hash->{PARMVALUE}; + Log3 $name, 5, "RenaultZE_AC_Step1 - In ".$hash."/".$name; + Log3 $name, 5, "RenaultZE_Set - value = >$value<"; + readingsSingleUpdate($hash,"ze_Step","RenaultZE_AC_Step1",1); + my $kamereon_api = $hash->{KAMEREON_API}; + my $id_token = $hash->{READINGS}{ze_Gigya_JWT_Token}{VAL}; + my $accId = $hash->{READINGS}{ze_Renault_AccId}{VAL}; + my $vin = $hash->{VIN}; + my $country = AttrVal($name,"ze_country","DE"); + Log3 $name, 5, "RenaultZE_AC_Step1 - Parms: ".$kamereon_api."/".$id_token; + + return undef if ( $id_token eq "" || $accId eq "" ); + + my $step1= { + 'Content-type' => 'application/vnd.api+json', + 'apikey' => $kamereon_api, + 'x-gigya-id_token' => $id_token + }; + + Log3 $name, 5, "RenaultZE_AC_Step1 - Data".$step1; + my $url= "empty"; + my $jsonData = "empty"; + if ( $value eq "on" ) + { + $jsonData = '{"data":{"type":"HvacStart","attributes":{"action":"start","targetTemperature":"21"}}}'; + $url = "https://api-wired-prod-1-euw1.wrd-aws.com/commerce/v1/accounts/".$accId."/kamereon/kca/car-adapter/v1/cars/".$vin."/actions/hvac-start?country=".$country; + } else { + $jsonData = '{"data":{"type":"HvacStart","attributes":{"action":"cancel"}}}'; + $url = "https://api-wired-prod-1-euw1.wrd-aws.com/commerce/v1/accounts/".$accId."/kamereon/kca/car-adapter/v1/cars/".$vin."/actions/hvac-start?country=".$country; + } + Log3 $name, 5, "RenaultZE_AC_Step1 - URL ".$url; + Log3 $name, 5, "RenaultZE_AC_Step1 - jsonData ".$jsonData; + my $param = { + url => $url, + header => $step1, + hash => $hash, + timeout => 15, + method => "POST", + data => $jsonData, + callback => \&RenaultZE_AC_Step2 + }; + + HttpUtils_NonblockingGet($param); # Starten der HTTP Abfrage. Es gibt keinen Return-Code. + Log3 $name, 5, "RenaultZE_AC_Step1 - Out"; + return undef; +} + +sub RenaultZE_AC_Step2($) +{ + my ($param, $err, $data) = @_; + my $hash = $param->{hash}; + my $name = $hash->{NAME}; + + Log3 $name, 5, "RenaultZE_AC_Step2 - In ".$hash."/".$name; + readingsSingleUpdate($hash,"ze_Step","RenaultZE_AC_Step2",1); + + RenaultZE_Error_err($hash,"RenaultZE_AC_Step2",$param->{url},$err,$data) if($err ne ""); + RenaultZE_Log_Data($hash,"RenaultZE_AC_Step2",$param->{url},$err,$data) if($data ne ""); + RenaultZE_Error_errorCode2($hash,"RenaultZE_AC_Step2",$param->{url},$err,$data) if($data =~ /error/); + + my $lastErr = $hash->{READINGS}{ze_lastErr}{VAL}; + return undef if ($lastErr ne ""); + + return undef if (RenaultZE_CheckJson($hash,$data)); + my $decode_json = from_json($data); + Log3 $name, 5, "RenaultZE_AC_Step2 - returned".$decode_json; + my $acstatus = $decode_json->{data}->{attributes}->{action}; + my $msg = "AC:".$acstatus; + Log3 $name, 5, "RenaultZE_AC_Step2 - acstatus=".$msg; + $hash->{STATE} = $msg; + #asyncOutput( $hash->{curCL}, $output ); + return undef; +} + +sub RenaultZE_Charge_Step1($) +{ + my ($hash) = @_; + my $name = $hash->{NAME}; + my $value = $hash->{PARMVALUE}; + Log3 $name, 5, "RenaultZE_Charge_Step1 - In ".$hash."/".$name; + Log3 $name, 5, "RenaultZE_Charge_Step1 - value = >$value<"; + readingsSingleUpdate($hash,"ze_Step","RenaultZE_Charge_Step1",1); + my $kamereon_api = $hash->{KAMEREON_API}; + my $id_token = $hash->{READINGS}{ze_Gigya_JWT_Token}{VAL}; + my $accId = $hash->{READINGS}{ze_Renault_AccId}{VAL}; + my $vin = $hash->{VIN}; + my $country = AttrVal($name,"ze_country","DE"); + Log3 $name, 5, "RenaultZE_Charge_Step1 - Parms: ".$kamereon_api."/".$id_token; + + return undef if ( $id_token eq "" || $accId eq "" ); + + my $step1= { + 'Content-type' => 'application/vnd.api+json', + 'apikey' => $kamereon_api, + 'x-gigya-id_token' => $id_token + }; + + Log3 $name, 5, "RenaultZE_Charge_Step1 - Data".$step1; + my $jsonData = "empty"; + my $url= "empty"; + if ( $value eq "start" ) + { + $jsonData = '{"data":{"type":"ChargingStart","attributes":{"action":"start"}}}'; + $url = "https://api-wired-prod-1-euw1.wrd-aws.com/commerce/v1/accounts/".$accId."/kamereon/kca/car-adapter/v1/cars/".$vin."/actions/charging-start?country=".$country; + } else { + $jsonData = '{"data":{"type":"ChargingStart","attributes":{"action":"stop"}}}'; + $url = "https://api-wired-prod-1-euw1.wrd-aws.com/commerce/v1/accounts/".$accId."/kamereon/kca/car-adapter/v1/cars/".$vin."/actions/charging-start?country=".$country; + } + Log3 $name, 5, "RenaultZE_Charge_Step1 - URL ".$url; + Log3 $name, 5, "RenaultZE_Charge_Step1 - jsonData ".$jsonData; + my $param = { + url => $url, + header => $step1, + hash => $hash, + timeout => 15, + method => "POST", + data => $jsonData, + callback => \&RenaultZE_Charge_Step2 + }; + + HttpUtils_NonblockingGet($param); # Starten der HTTP Abfrage. Es gibt keinen Return-Code. + Log3 $name, 5, "RenaultZE_Charge_Step1 - Out"; + return undef; +} + +sub RenaultZE_Charge_Step2($) +{ + my ($param, $err, $data) = @_; + my $hash = $param->{hash}; + my $name = $hash->{NAME}; + + Log3 $name, 5, "RenaultZE_Charge_Step2 - In ".$hash."/".$name; + readingsSingleUpdate($hash,"ze_Step","RenaultZE_Charge_Step2",1); + + RenaultZE_Error_err($hash,"RenaultZE_Charge_Step2",$param->{url},$err,$data) if($err ne ""); + RenaultZE_Log_Data($hash,"RenaultZE_Charge_Step2",$param->{url},$err,$data) if($data ne ""); + RenaultZE_Error_errorCode2($hash,"RenaultZE_Charge_Step2",$param->{url},$err,$data) if($data =~ /error/); + + my $lastErr = $hash->{READINGS}{ze_lastErr}{VAL}; + return undef if ($lastErr ne ""); + + return undef if (RenaultZE_CheckJson($hash,$data)); + my $decode_json = from_json($data); + Log3 $name, 5, "RenaultZE_Charge_Step2 - returned".$decode_json; + my $chargestatus = $decode_json->{data}->{attributes}->{action}; + my $msg = "Charge:".$chargestatus; + Log3 $name, 5, "RenaultZE_Charge_Step2 - chargestatus=".$msg; + $hash->{STATE} = $msg; + #asyncOutput( $hash->{curCL}, $output ); + return undef; +} + +sub RenaultZE_Log_Data($$$$$) +{ + my ($hash, $step, $url, $err, $data) = @_; + my $name = $hash->{NAME}; + Log3 $name, 5, "INFO: ".$step.", url: ".$url.", data: ".$data.", error: ".$err; + + $err = RenaultZE_pp_err($hash,$err) if($err ne "");; + + readingsSingleUpdate($hash,"ze_lastUrl",$url,1); + readingsSingleUpdate($hash,"ze_lastErr",$err,1); + readingsSingleUpdate($hash,"ze_lastData",$data,1); + return undef; +} + +sub RenaultZE_Error_err($$$$$) +{ + my ($hash, $step, $url, $err, $data) = @_; + my $name = $hash->{NAME}; + Log3 $name, 3, "ERROR: ".$step.", error while calling ".$url." - $err"; + + $err = RenaultZE_pp_err($hash,$err) if($err ne "");; + + readingsSingleUpdate($hash,"ze_lastUrl",$url,1); + readingsSingleUpdate($hash,"ze_lastErr",$err,1); + readingsSingleUpdate($hash,"ze_lastData",$data,1); +} + +sub RenaultZE_Error_errorCode1($$$$$) +{ + my ($hash, $step, $url, $err, $data) = @_; + my $name = $hash->{NAME}; + return undef if (RenaultZE_CheckJson($hash,$data)); + my $decode_json = from_json($data); + my $errorCode = $decode_json->{errorCode}; + my $errorDetails = $decode_json->{errorDetails}; + my $errorMessage = $decode_json->{errorMessage}; + my $statusReason = $decode_json->{statusReason}; + my $msg = "errorCode=".$errorCode.", errorDetails=".$errorDetails.", errorMessage=".$errorMessage.", statusReason=".$statusReason; + Log3 $name, 3, "ERROR: (1) ".$step.", errorCode while calling ".$url." - $msg"; + + $msg = RenaultZE_pp_err($hash,$errorMessage) if($errorMessage ne "");; + + readingsSingleUpdate($hash,"ze_lastUrl",$url,1); + readingsSingleUpdate($hash,"ze_lastErr",$msg,1); + readingsSingleUpdate($hash,"ze_lastData",$data,1); +} + + +sub RenaultZE_Error_errorCode2($$$$$) +{ + my ($hash, $step, $url, $err, $data) = @_; + my $name = $hash->{NAME}; + return undef if (RenaultZE_CheckJson($hash,$data)); + my $decode_json = from_json($data); + my $errorCode = $decode_json->{errors}[0]->{errorCode}; + my $errorMessage = $decode_json->{errors}[0]->{errorMessage}; + my $msg = "errorCode=".$errorCode.", errorMessage=".$errorMessage; + Log3 $name, 3, "ERROR: (2) ".$step.", error (data) while calling ".$url." - $msg"; + + $msg = RenaultZE_pp_err($hash,$msg) if($msg ne "");; + + readingsSingleUpdate($hash,"ze_lastUrl",$url,1); + readingsSingleUpdate($hash,"ze_lastErr",$msg,1); + readingsSingleUpdate($hash,"ze_lastData",$data,1); +} + +sub RenaultZE_pp_err($$) +{ + my ($hash,$err) = @_; + my $name = $hash->{NAME}; + Log3 $name, 3, "INFO: pretty printing error ".$err; + my $errj = $err; + $errj =~ s/.*errorMessage=//g; + my $json_out = eval { decode_json($errj) }; + my $output = ""; + if ($@) + { + $output = $err; + } else { + my $decode_json = from_json($errj); + my $mtab = $decode_json->{errors}; + $output = "Error"; + $output = $output.""; + $output = $output.""; + foreach my $item( @$mtab ) { + $output = $output.""; + $output = $output.""; + $output = $output.""; + $output = $output.""; + } + $output = $output."
raw".$err."
status".$item->{status}."
code".$item->{code}."
title".$item->{title}."
detail".$item->{detail}."
"; + } + return $output; +} +sub RenaultZE_distanceFromHome($$$) +{ + my ($hash, $lat, $long) = @_; + my $name = $hash->{NAME}; + my $hlong = AttrVal( $name, "ze_longitude", AttrVal( "global", "longitude", 0.0 ) ); + my $hlat = AttrVal( $name, "ze_latitude", AttrVal( "global", "latitude", 0.0 ) ); + + #Kreiszahl Pi + my $pi = 3.14159; + + #Umrechnung von Grad in Radius + my $long1 = $long / 180 * $pi; + my $lat1 = $lat / 180 * $pi; + my $long2 = $hlong / 180 * $pi; + my $lat2 = $hlat / 180 * $pi; + + #Entfernungsberechnung + my $distance = acos(sin($long1)*sin($long2) + cos($long1)*cos($long2)*cos($lat2-$lat1)); + + #Erdrundung einbeziehen + $distance = $distance * 6378.137; + + my $dim = "km"; + my $homeinfo = ""; + my $homestate = "away"; + + if ($distance < 1) { + $distance = $distance * 1000; + $dim = "m"; + if ($distance < 20) { + $homeinfo = "home"; + $homestate = "home"; + } + } + $distance = sprintf("%.3f", $distance); + $homeinfo = $distance." ".$dim." away" if ( $homeinfo eq ""); + if ( $hlong != 0.0 and $hlat != 0.0) { + readingsSingleUpdate($hash,"distanceFromHome",$distance,1); + readingsSingleUpdate($hash,"distanceUnit",$dim,1); + readingsSingleUpdate($hash,"homeInfo",$homeinfo,1); + readingsSingleUpdate($hash,"homeState",$homestate,1); + RenaultZE_gAddress1($hash,$lat,$long) if (AttrVal($name,"ze_showaddress","1") eq 1); + } + + + return undef; +} + +sub RenaultZE_gAddress1($$$) +{ + my ($hash,$lat,$long) = @_; + my $name = $hash->{NAME}; + + Log3 $name, 5, "RenaultZE_gAddress1 - In ".$hash."/".$name." ".$lat."/".$long; + readingsSingleUpdate($hash,"ze_Step","RenaultZE_gAddress1",1); + + my $url = "https://www.google.com/maps/place/$lat+$long"; + Log3 $name, 5, "RenaultZE_gData_Step1 - URL ".$url; + my $param = { + url => $url, + hash => $hash, + timeout => 15, + method => "GET", + callback => \&RenaultZE_gAddress2 + }; + + HttpUtils_NonblockingGet($param); # Starten der HTTP Abfrage. Es gibt keinen Return-Code. + Log3 $name, 5, "RenaultZE_gAddress1 - Out"; + return 0; +} + +sub RenaultZE_gAddress2($) +{ + my ($param, $err, $data) = @_; + my $hash = $param->{hash}; + my $name = $hash->{NAME}; + + Log3 $name, 5, "RenaultZE_gAddress2 - In ".$hash."/".$name; + Log3 $name, 5, "RenaultZE_gAddress2 - In err".$hash."/".$err; + Log3 $name, 5, "RenaultZE_gAddress2 - In data".$hash."/".$data; + readingsSingleUpdate($hash,"ze_Step","RenaultZE_gAddress2",1); + + # RenaultZE_Error_err($hash,"RenaultZE_gAddress2",$param->{url},$err,$data) if($err ne ""); + # RenaultZE_Log_Data($hash,"RenaultZE_gAddress2",$param->{url},$err,$data) if($data ne ""); + # RenaultZE_Error_errorCode2($hash,"RenaultZE_gAddress2",$param->{url},$err,$data) if($data =~ /error/); + + # my $lastErr = $hash->{READINGS}{ze_lastErr}{VAL}; + # return undef if ($lastErr ne ""); + + $data =~ s/.*meta content=\"(.*)\" itemprop=\"description\".*/$1/sg; + Log3 $name, 5, "RenaultZE_gAddress2 - Address ".$data; + my $oldinfo = ReadingsVal($name,"homeInfo",""); + my $newinfo = $oldinfo." (".$data.")"; + readingsSingleUpdate($hash,"homeInfo",$newinfo,1); + + Log3 $name, 5, "RenaultZE_gAddress2 - Out"; +} + +sub RenaultZE_CheckJson($$) +{ + my ($hash,$json) = @_; + my $name = $hash->{NAME}; + my $json_out = eval { decode_json($json) }; + if ($@) + { + readingsSingleUpdate($hash,"ze_lastErr","unexpected json error",1); + readingsSingleUpdate($hash,"ze_lastData",$json,1); + return 1; + } + return 0; +} + +sub RenaultZE_EpochFromDateTime($) { + my ($timestamp) = @_; + my $t; + + if ( substr($timestamp,-1,1) eq "Z" ) + { + $t = eval { Time::Piece->strptime($timestamp, "%Y-%m-%dT%H:%M:%SZ")->epoch }; + } + elsif ( substr($timestamp,-3,1) eq ":" ) + { + $timestamp =~ s/\+(\d{2}):(\d{2})$/+$1$2/; + $timestamp =~ s/\.\d{3}+/+/; + $t = eval { Time::Piece->strptime($timestamp, "%Y-%m-%dT%H:%M:%S%z")->epoch }; + } + elsif ( substr($timestamp,-5,5) =~ /\+(\d{4})/ ) + { + $t = eval { Time::Piece->strptime($timestamp, "%Y-%m-%dT%H:%M:%S%z")->epoch }; + } + + $t = 0 if ($t eq ""); + + return $t; +} + +############################## +1; + +=pod +=begin html + + +

RenaultZE

+
    + RenaultZE implements an interface to the Renualt ZE API
    +

    + + Define +
      + define <name> RenaultZE <VIN> <Interval> +

      + Example: define myZoe RenaultZE VF1....... 300 +

      + VIN is the Vehicle Identification Number (you'll find it in your registration)
      + Interval is the interval in seconds between status updates +
    +
    + + + Set
    +
      + set <name> <option> <value> +

      + You can set
      +
        +
      • password
        + enter your Renault ZE accounts password
      • +
      • AC
        + either on or cancel (which probably only cancles a timer)
      • +
      • Charge
        + either on or off (off doesn't work if you have set 'charge mode walways')
      • +
      • state
        +
      +
    +
    + + + Get
    +
      + get <name> <option> +

      + You can get
      +
        +
      • charge-history
        + summary of charges, parameter options:
        + type=day&start=YYYYMMDD&end=YYYYMMDD (default: 1.1.2000 till today)
        + type=month&start=YYYYMM&end=YYYYMM +
      • +
      • charges
        + only for Phase1 models
        + list of charges, parameter options:
        + start=YYYYMMDD&end=YYYYMMD (default: 1.1.2000 till today) +
      • +
      • charging-settings
        + lists the settings for charging +
      • +
      • hvac-history
        + only for Phase1 models
        + shows the air condition history, parameter options:
        + type=day&start=YYYYMMDD&end=YYYYMMDD (default: 1.1.2000 till today)
        + type=month&start=YYYYMM&end=YYYYMM +
      • +
      • notification-settings
        + only for Phase1 models
        + lists the settings for cnotifications +
      • +
      • update
        + force update of the current readings
      • +
      • vehicles
        + get a list of your vehicles with details, set the readings for the images
        + if the attribute ze_showimage is set you get readings with the cars images
      • +
      • zTest
        + Option to test new API functions which might be implemented one day ...
        + sub parameters are
        + hvac-sessions?start=20201101&end=20210108&country=DE
        + charges?start=20201101&end=20210108&country=DE
        + charge-history?type=day&start=20201101&end=20210108&country=DE
        + charge-history?type=month&start=202011&end=202101&country=DE
        + lock-status?country=DE
        + As result you will either get a msgbox when the function is implemented by Renault, otherwise the ze-Readings will tell you more +
      • +
      +
    +
    + + + Attributes +
      + attr <name> <attribute> <value> +

      + See commandref#attr for more info about + the attr command. +

      + Attributes: +
        +
      • ze_phase 1|2
        + The phase of ZE technology, either 1 or 2, right now only phase 2 is supported +
      • +
      • ze_country
        + 2 letter country code, e.g. DE ot GB +
      • +
      • ze_user
        + The user-id that you used to register at Renault +
      • +
      • ze_latitude
        + Latitude of your home location. Is being used to calculate homeInfo. Function also checks for global attribte latitude als default. +
      • +
      • ze_longitude
        + Longitude of your home location. Is being used to calculate homeInfo. Function also checks for global attribte longitude als default. +
      • +
      • ze_showaddress
        + Retrieve address via reverse geocoding fromn Google Maps and add it to homeInfo. +
      • +
      • ze_showimage
        + Show the image of the car that you get from vehicles as reading
        + 0 = off + 1 = only the small image (default)
        + 2 = both, small and large image +
      • +
      +
    +
+ +=end html + +=cut