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;