From 55ecec4aa46e657ed24b74da25dc1ff0a5549ba2 Mon Sep 17 00:00:00 2001 From: sebastianschwarz Date: Wed, 26 Feb 2025 21:29:42 +0100 Subject: [PATCH] erster push, no usable code --- CHANGED | 1 + FHEM/73_Bluelink.pm | 209 ++++++++++++++++++++++++++ controls_BlueLink.txt | 2 + hooks/pre-commit | 43 ++++++ lib/FHEM/Devices/Bluelink/Bluelink.pm | 198 ++++++++++++++++++++++++ lib/FHEM/Devices/Bluelink/Hyundai.pm | 113 ++++++++++++++ 6 files changed, 566 insertions(+) create mode 100644 CHANGED create mode 100644 FHEM/73_Bluelink.pm create mode 100644 controls_BlueLink.txt create mode 100755 hooks/pre-commit create mode 100644 lib/FHEM/Devices/Bluelink/Bluelink.pm create mode 100644 lib/FHEM/Devices/Bluelink/Hyundai.pm 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 + + +=end html +=begin html_DE + + +

Bluelink

+
+ +Define + + +=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; +