erster push, no usable code

This commit is contained in:
sebastianschwarz 2025-02-26 21:29:42 +01:00
parent 1655c6645a
commit 55ecec4aa4
6 changed files with 566 additions and 0 deletions

1
CHANGED Normal file
View File

@ -0,0 +1 @@
initial push

209
FHEM/73_Bluelink.pm Normal file
View File

@ -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
<a name="Bluelink"></a>
<h3>Bluelink</h3>
<a name="Bluelinkdefine"></a>
<b>Define</b>
<ul><br>
<code>define &lt;name&gt; Bluelink </code>
<br><br>
Example:
<ul><br>
<code>define bl Bluelink </code><br>
</ul>
<br><br>
<a name="Bluelinkreadings"></a>
<br><br>
<b>Readings</b>
<ul>
<li>1st</li>
<li>2nd</li>
</ul>
<br><br>
<a name="Bluelinkeset"></a>
<b>set</b>
<ul>
<li>first</li>
<li>secibd</li>
</ul>
<br><br>
<a name="Bluelinkeattributes"></a>
<b>Attributes</b>
<ul>
<li>foo</li>
<li>bar</li>
</ul>
</ul>
=end html
=begin html_DE
<a name="Bluelink"></a>
<h3>Bluelink</h3>
<br>
<a name="Bluelinkdefine"></a>
<b>Define</b>
<ul><br>
<code>define &lt;name&gt; Bluelink</code>
<br><br>
Beispiel:
<ul><br>
<code>define bl Bluelink</code><br>
</ul>
<br><br>
<a name="Bluelinkreadings"></a>
<br><br>
<b>Readings</b>
<ul>
<li>1st</li>
<li>2nd</li>
</ul>
<br><br>
<a name="Bluelinkset"></a>
<b>set</b>
<ul>
<li>erstens </li>
<li>zweitens</li>
</ul>
<br><br>
<a name="Bluelinkeattributes"></a>
<b>Attributes</b>
<ul>
<li>foo</li>
<li>bar</li>
</ul>
</ul>
=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 <ema@il.local>"
],
"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

2
controls_BlueLink.txt Normal file
View File

@ -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

43
hooks/pre-commit Executable file
View File

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

View File

@ -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 <name> Bluelink <API_URL>" 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;

View File

@ -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 <name> BlueLinkVehicle <VIN> <IODev>" 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;