diff --git a/CHANGED b/CHANGED
new file mode 100644
index 0000000..2d7881b
--- /dev/null
+++ b/CHANGED
@@ -0,0 +1 @@
+ initial push
diff --git a/FHEM/73_Bluelink.pm b/FHEM/73_Bluelink.pm
new file mode 100644
index 0000000..283ade2
--- /dev/null
+++ b/FHEM/73_Bluelink.pm
@@ -0,0 +1,209 @@
+###############################################################################
+#
+# Developed with love
+#
+# (c) 2025-2025 Copyright: Sebastian Schwarz
+#
+# 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
+# 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.
+#
+#
+# $Id$
+#
+###############################################################################
+
+package FHEM::Bluelink;
+
+use strict;
+use warnings;
+
+use GPUtils qw(GP_Import GP_Export);
+require FHEM::Devices::Bluelink::Bluelink;
+
+## Import der FHEM Funktionen
+#-- Run before package compilation
+BEGIN {
+
+ # Import from main context
+ GP_Import(
+ qw(
+ readingFnAttributes
+ )
+ );
+}
+
+#-- Export to main context with different name
+GP_Export(
+ qw(
+ Initialize
+ )
+);
+
+sub Initialize {
+ my $hash = shift;
+
+ # Provider
+ $hash->{WriteFn} = \&Write;
+
+ # Consumer
+ $hash->{DefFn} = 'FHEM::Devices::Bluelink::Bluelink::Define';
+ $hash->{UndefFn} = 'FHEM::Devices::Bluelink::Bluelink::Undef';
+ $hash->{SetFn} = 'FHEM::Devices::Bluelink::Bluelink::Set';
+ $hash->{DeleteFn} = 'FHEM::Devices::Bluelink::Bluelink::Delete';
+ $hash->{RenameFn} = 'FHEM::Devices::Bluelink::Bluelink::Rename';
+ $hash->{NotifyFn} = 'FHEM::Devices::Bluelink::Bluelink::Notify';
+
+
+ $hash->{AttrFn} = 'FHEM::Devices::Bluelink::Bluelink::Attr';
+ $hash->{AttrList} =
+ 'disable:1 '
+ . 'interval '
+ . $::readingFnAttributes;
+ $hash->{parseParams} = 1;
+
+ return FHEM::Meta::InitMod( __FILE__, $hash );
+}
+
+1;
+
+=pod
+
+=item device
+=item summary Modul to control Bluelink Vehicles
+=item summary_DE Modul zur Steuerung von Bluelink Fahrzeugen
+=begin html
+
+
+
Bluelink
+
+Define
+
+ define <name> Bluelink
+
+ Example:
+
+
+
+
+ Readings
+
+
+
+ set
+
+
+
+ Attributes
+
+
+
+=end html
+=begin html_DE
+
+
+Bluelink
+
+
+Define
+
+ define <name> Bluelink
+
+ Beispiel:
+
+
+
+
+ Readings
+
+
+
+ set
+
+
+
+ Attributes
+
+
+
+
+=end html_DE
+
+=for :application/json;q=META.json 73_Bluelink.pm
+{
+ "abstract": "Modul to control Bluelink",
+ "x_lang": {
+ "de": {
+ "abstract": "Modul zum bedienen des Bluelink"
+ }
+ },
+ "keywords": [
+ "fhem-mod-device",
+ "fhem-core",
+ "Bluelink",
+ "Smart"
+ ],
+ "release_status": "stable",
+ "license": "GPL_2",
+ "version": "v0.0.1",
+ "author": [
+ "Sebastian Schwarz "
+ ],
+ "x_fhem_maintainer": [
+ "BOFH"
+ ],
+ "x_fhem_maintainer_github": [
+ "NO ONE"
+ ],
+ "prereqs": {
+ "runtime": {
+ "requires": {
+ "FHEM": 5.00918799,
+ "perl": 5.016,
+ "Meta": 0,
+ "HttpUtils": 0,
+ "Encode": 0
+ },
+ "recommends": {
+ },
+ "suggests": {
+ }
+ }
+ }
+}
+=end :application/json;q=META.json
+
+=cut
diff --git a/controls_BlueLink.txt b/controls_BlueLink.txt
new file mode 100644
index 0000000..dfab368
--- /dev/null
+++ b/controls_BlueLink.txt
@@ -0,0 +1,2 @@
+UPD 2025-02-26_15:50:12 4327 FHEM/73_BlueLink.pm
+UPD 2025-02-26_13:26:10 3120 lib/FHEM/Devices/BlueLink/Hyundai.pm
diff --git a/hooks/pre-commit b/hooks/pre-commit
new file mode 100755
index 0000000..47dc589
--- /dev/null
+++ b/hooks/pre-commit
@@ -0,0 +1,43 @@
+#!/usr/bin/perl -w
+
+use File::Basename;
+use POSIX qw(strftime);
+use strict;
+
+my @filenames = (
+ 'FHEM/73_GoECharger.pm',
+ 'lib/FHEM/Devices/GoE/GoECharger.pm',
+ 'www/images/fhemSVG/goecharger.svg'
+);
+
+my $controlsfile = 'controls_GoECharger.txt';
+
+open(FH, ">$controlsfile") || return("Can't open $controlsfile: $!");
+
+for my $filename (@filenames) {
+ my @statOutput = stat($filename);
+
+ if (scalar @statOutput != 13) {
+ printf 'error: stat has unexpected return value for ' . $filename . "\n";
+ next;
+ }
+
+ my $mtime = $statOutput[9];
+ my $date = POSIX::strftime("%Y-%m-%d", localtime($mtime));
+ my $time = POSIX::strftime("%H:%M:%S", localtime($mtime));
+ my $filetime = $date."_".$time;
+
+ my $filesize = $statOutput[7];
+
+ printf FH 'UPD ' . $filetime . ' ' . $filesize . ' ' .$filename . "\n";
+}
+
+close(FH);
+
+system("git log -1 | tail -n1 > CHANGED");
+system("git add CHANGED");
+system("git add $controlsfile");
+
+print 'Create controls File succesfully' . "\n";
+
+exit 0;
diff --git a/lib/FHEM/Devices/Bluelink/Bluelink.pm b/lib/FHEM/Devices/Bluelink/Bluelink.pm
new file mode 100644
index 0000000..0949e3c
--- /dev/null
+++ b/lib/FHEM/Devices/Bluelink/Bluelink.pm
@@ -0,0 +1,198 @@
+package FHEM::Bluelink;
+
+use strict;
+use warnings;
+use LWP::UserAgent;
+use HTTP::Request::Common qw(GET POST);
+use JSON;
+use MIME::Base64;
+use Digest::SHA qw(sha256_hex);
+use Time::HiRes qw(time);
+
+sub Initialize {
+ my ($hash) = @_;
+
+ $hash->{DefFn} = "Bluelink_Define";
+ $hash->{UndefFn} = "Bluelink_Undef";
+ $hash->{RenameFn} = "Bluelink_Rename";
+ $hash->{DeleteFn} = "Bluelink_Delete";
+ $hash->{NotifyFn} = "Bluelink_Notify";
+
+ $hash->{SetFn} = "Bluelink_Set";
+ $hash->{GetFn} = "Bluelink_Get";
+ $hash->{AttrList} = "username password language pin client_id client_secret access_token refresh_token";
+}
+
+sub Bluelink_Define {
+ my ($hash, $def) = @_;
+ my @args = split("[ \t]+", $def);
+
+ return "Usage: define Bluelink " if (@args < 3);
+
+ $hash->{API_URL} = $args[2];
+ return;
+}
+
+sub Bluelink_Undef {
+ my ($hash) = @_;
+ return;
+}
+
+sub Bluelink_Set {
+ my ($hash, $name, $cmd, @args) = @_;
+
+ return "\"set $name\" needs at least one argument" unless defined $cmd;
+
+ if ($cmd eq "login") {
+ return Bluelink_Login($hash);
+ } elsif ($cmd eq "refreshToken") {
+ return Bluelink_RefreshToken($hash);
+ } elsif ($cmd eq "setLanguage") {
+ return Bluelink_SetLanguage($hash, $args[0]);
+ }
+
+ return "Unknown argument $cmd, choose one of login refreshToken setLanguage";
+}
+
+sub Bluelink_Get {
+ my ($hash, $name, $cmd, @args) = @_;
+
+ return "\"get $name\" needs at least one argument" unless defined $cmd;
+
+ if ($cmd eq "deviceid") {
+ return Bluelink_GetDeviceID($hash);
+ } elsif ($cmd eq "token") {
+ return AttrVal($name, "access_token", "No token available");
+ }
+
+ return "Unknown argument $cmd, choose one of deviceid token";
+}
+
+sub Bluelink_GetDeviceID {
+ my ($hash) = @_;
+ my $api_url = $hash->{API_URL} . "/api/v1/spa/notifications/register";
+
+ my $uuid = lc(sha256_hex(time() . rand()));
+ my $data = {
+ pushRegId => uc(unpack("H*", pack("C*", map { int(rand(16)) } (1..32)))),
+ pushType => "GCM",
+ uuid => $uuid
+ };
+
+ my $res = Bluelink_HttpPost($api_url, $data);
+
+ return "Device ID: " . ($res->{ResMsg}->{DeviceID} // "Not found");
+}
+
+sub Bluelink_Login {
+ my ($hash) = @_;
+ my $username = AttrVal($hash->{NAME}, "username", "");
+ my $password = AttrVal($hash->{NAME}, "password", "");
+ my $api_url = $hash->{API_URL} . "/api/v1/user/signin";
+
+ return "Missing username or password" unless $username && $password;
+
+ my $data = { email => $username, password => $password };
+ my $res = Bluelink_HttpPost($api_url, $data);
+
+ return "Login failed: " . $res->{errMsg} if $res->{errCode};
+
+ my $redirect_url = $res->{redirectUrl};
+ my $code = (split(/\?code=/, $redirect_url))[1];
+
+ return Bluelink_ExchangeCode($hash, $code);
+}
+
+sub Bluelink_ExchangeCode {
+ my ($hash, $code) = @_;
+ my $api_url = $hash->{API_URL} . "/api/v1/user/oauth2/token";
+ my $client_id = AttrVal($hash->{NAME}, "client_id", "");
+ my $client_secret = AttrVal($hash->{NAME}, "client_secret", "");
+
+ return "Missing client_id or client_secret" unless $client_id && $client_secret;
+
+ my $auth = encode_base64("$client_id:$client_secret", "");
+
+ my $data = {
+ grant_type => "authorization_code",
+ redirect_uri => $hash->{API_URL} . "/api/v1/user/oauth2/redirect",
+ code => $code
+ };
+
+ my $res = Bluelink_HttpPost($api_url, $data, { Authorization => "Basic $auth" });
+
+ if ($res->{access_token}) {
+ fhem("attr $hash->{NAME} access_token " . $res->{access_token});
+ fhem("attr $hash->{NAME} refresh_token " . $res->{refresh_token});
+ return "Login successful. Token saved.";
+ }
+
+ return "Token exchange failed.";
+}
+
+sub Bluelink_RefreshToken {
+ my ($hash) = @_;
+ my $api_url = $hash->{API_URL} . "/api/v1/user/oauth2/token";
+ my $refresh_token = AttrVal($hash->{NAME}, "refresh_token", "");
+ my $client_id = AttrVal($hash->{NAME}, "client_id", "");
+ my $client_secret = AttrVal($hash->{NAME}, "client_secret", "");
+
+ return "Missing refresh_token, client_id or client_secret" unless $refresh_token && $client_id && $client_secret;
+
+ my $auth = encode_base64("$client_id:$client_secret", "");
+
+ my $data = {
+ grant_type => "refresh_token",
+ redirect_uri => "https://www.getpostman.com/oauth2/callback",
+ refresh_token => $refresh_token
+ };
+
+ my $res = Bluelink_HttpPost($api_url, $data, { Authorization => "Basic $auth" });
+
+ if ($res->{access_token}) {
+ fhem("attr $hash->{NAME} access_token " . $res->{access_token});
+ fhem("attr $hash->{NAME} refresh_token " . $res->{refresh_token});
+ return "Token refreshed.";
+ }
+
+ return "Token refresh failed.";
+}
+
+sub Bluelink_SetLanguage {
+ my ($hash, $language) = @_;
+ my $api_url = $hash->{API_URL} . "/api/v1/user/language";
+
+ my $data = { lang => $language };
+
+ my $res = Bluelink_HttpPost($api_url, $data);
+
+ return "Language set to $language" if $res;
+
+ return "Failed to set language.";
+}
+
+sub Bluelink_HttpPost {
+ my ($url, $data, $headers) = @_;
+
+ my $ua = LWP::UserAgent->new();
+ my $json_data = encode_json($data);
+
+ my $req = HTTP::Request->new(POST => $url);
+ $req->header("Content-Type" => "application/json");
+ $req->content($json_data);
+
+ if ($headers) {
+ foreach my $key (keys %$headers) {
+ $req->header($key => $headers->{$key});
+ }
+ }
+
+ my $res = $ua->request($req);
+
+ return decode_json($res->decoded_content) if $res->is_success;
+
+ return { error => "Request failed", status => $res->code };
+}
+
+1;
+
diff --git a/lib/FHEM/Devices/Bluelink/Hyundai.pm b/lib/FHEM/Devices/Bluelink/Hyundai.pm
new file mode 100644
index 0000000..0ac0e9c
--- /dev/null
+++ b/lib/FHEM/Devices/Bluelink/Hyundai.pm
@@ -0,0 +1,113 @@
+package FHEM::BlueLinkVehicle;
+
+use strict;
+use warnings;
+use JSON;
+use Time::Piece;
+
+sub Initialize {
+ my ($hash) = @_;
+
+ $hash->{DefFn} = "BlueLinkVehicle_Define";
+ $hash->{SetFn} = "BlueLinkVehicle_Set";
+ $hash->{GetFn} = "BlueLinkVehicle_Get";
+ $hash->{AttrList} = "vin IODev";
+}
+
+sub BlueLinkVehicle_Define {
+ my ($hash, $def) = @_;
+ my @args = split("[ \t]+", $def);
+
+ return "Usage: define BlueLinkVehicle " if (@args < 4);
+
+ $hash->{VIN} = $args[2];
+ my $parent = $args[3];
+ $hash->{IODev} = $defs{$parent};
+
+ return "Parent device $parent not found" unless defined $hash->{IODev};
+
+ return;
+}
+
+sub BlueLinkVehicle_Set {
+ my ($hash, $name, $cmd, @args) = @_;
+
+ return "\"set $name\" needs at least one argument" unless defined $cmd;
+
+ if ($cmd eq "update") {
+ return BlueLinkVehicle_Update($hash);
+ }
+
+ return "Unknown argument $cmd, choose one of update";
+}
+
+sub BlueLinkVehicle_Get {
+ my ($hash, $name, $cmd, @args) = @_;
+
+ return "\"get $name\" needs at least one argument" unless defined $cmd;
+
+ if ($cmd eq "soc") {
+ return ReadingsVal($name, "SoC", "Unknown") . " %";
+ } elsif ($cmd eq "range") {
+ return ReadingsVal($name, "Range", "Unknown") . " km";
+ } elsif ($cmd eq "status") {
+ return ReadingsVal($name, "Status", "Unknown");
+ }
+
+ return "Unknown argument $cmd, choose one of soc range status";
+}
+
+sub BlueLinkVehicle_Update {
+ my ($hash) = @_;
+ my $name = $hash->{NAME};
+ my $parent = $hash->{IODev};
+ my $vin = $hash->{VIN};
+
+ return "Parent module not found" unless $parent;
+ return "VIN not defined" unless $vin;
+
+ # Anfrage an das Hauptmodul senden (IOWrite)
+ IOWrite($parent, "getVehicleStatus", $vin);
+ return "Request sent to parent module.";
+}
+
+sub BlueLinkVehicle_ParseTime {
+ my ($time_string) = @_;
+ return unless $time_string;
+
+ my $format = "%Y%m%d%H%M%S %z";
+ my $t = Time::Piece->strptime($time_string . " +0100", $format);
+
+ return $t->strftime("%Y-%m-%d %H:%M:%S");
+}
+
+sub BlueLinkVehicle_Receive {
+ my ($hash, $data) = @_;
+ my $name = $hash->{NAME};
+
+ my $json = eval { decode_json($data) };
+ return "Invalid JSON data received" if $@;
+
+ if ($json->{vin} && $json->{vin} eq $hash->{VIN}) {
+ my $status = $json->{status};
+
+ my $soc = $status->{EvStatus}->{BatteryStatus} // "Unknown";
+ my $range = $status->{EvStatus}->{DrvDistance}->[0]->{RangeByFuel}->{EvModeRange}->{Value} // "Unknown";
+ my $charging = $status->{EvStatus}->{BatteryCharge} ? "Charging" : "Not Charging";
+ my $updated = BlueLinkVehicle_ParseTime($status->{Time}) // "Unknown";
+
+ readingsBeginUpdate($hash);
+ readingsBulkUpdate($hash, "SoC", $soc);
+ readingsBulkUpdate($hash, "Range", $range);
+ readingsBulkUpdate($hash, "Status", $charging);
+ readingsBulkUpdate($hash, "LastUpdate", $updated);
+ readingsEndUpdate($hash, 1);
+
+ return "Vehicle data updated.";
+ }
+
+ return "VIN mismatch - ignoring data.";
+}
+
+1;
+