Suche Entwickler für Velux Modul

Begonnen von Mitch, 31 August 2023, 20:56:59

Vorheriges Thema - Nächstes Thema

Mitch

Hallo,

ich suchen einen Entwickler, der Lust hätte ein Modul für Velux zu schreiben.
Gerne auch gegen eine Spende!

API vorhanden, basiert auf dei Netatmo API.
https://github.com/nougad/velux-cli
https://github.com/Droccal/homebridge-velux-active/tree/master
FHEM im Proxmox Container

Mitch

FHEM im Proxmox Container

Mitch

Nachdem wohl niemand Lust hatte und ich leider auch immer noch nicht Perl "sprechen" kann, habe ich mir von der KI helfen lassen.
Das Modul verbindet sich, liest alle Geräte ein und ich kann sie auch steuern.
Die Readings sind noch etwas unübersichtlich, aber erstmal ein Anfang.

Evtl. mag ja ein "Profi" mal drüber schauen?

package main;

use strict;
use warnings;
use HttpUtils;
use JSON;

sub VeluxKIX300_Initialize {
    my ($hash) = @_;

    $hash->{DefFn}    = 'VeluxKIX300_Define';
    $hash->{UndefFn}  = 'VeluxKIX300_Undef';
    $hash->{SetFn}    = 'VeluxKIX300_Set';
    $hash->{GetFn}    = 'VeluxKIX300_Get';
    $hash->{AttrList} = "interval " . $readingFnAttributes;
}

sub VeluxKIX300_Define {
    my ($hash, $def) = @_;
    my @a = split("[ \t][ \t]*", $def);

    return "wrong syntax: define <name> VeluxKIX300 <username> <password>" if (int(@a) != 4);

    my ($name, $type, $username, $password) = @a;

    $hash->{NAME}      = $name;
    $hash->{TYPE}      = $type;
    $hash->{USERNAME}  = $username;
    $hash->{PASSWORD}  = $password;
    $hash->{CLIENT_ID} = "5931426da127d981e76bdd3f";
    $hash->{CLIENT_SECRET} = "6ae2d89d15e767ae5c56b456b452d319";
    $hash->{INTERVAL}  = AttrVal($name, 'interval', 300);

    Log3 $name, 3, "VeluxKIX300 ($name) - Module initialized.";

    VeluxKIX300_Authenticate($hash);

    InternalTimer(gettimeofday() + $hash->{INTERVAL}, "VeluxKIX300_GetHomeData", $hash, 0);

    return undef;
}

sub VeluxKIX300_Undef {
    my ($hash, $name) = @_;
    RemoveInternalTimer($hash);
    return undef;
}

sub VeluxKIX300_Set {
    my ($hash, @a) = @_;
    my $name = $hash->{NAME};

    return "\"set $name\" needs at least one argument" if (@a < 2);

    my $cmd = $a[1];
    if ($cmd eq "update") {
        Log3 $name, 3, "VeluxKIX300 ($name) - Set command: update";
        VeluxKIX300_GetHomeData($hash);
        return undef;
    } elsif ($cmd eq "open" || $cmd eq "close" || $cmd eq "set_position" || $cmd eq "retrieve_key" || $cmd eq "stop_movements") {
        my $device = $a[2] // return "Device ID required for $cmd command";
        my $position = $a[3] if ($cmd eq "set_position");
        Log3 $name, 3, "VeluxKIX300 ($name) - Set command: $cmd for device $device";
        VeluxKIX300_ControlDevice($hash, $cmd, $device, $position);
        return undef;
    } else {
        return "Unknown argument $cmd, choose one of update open close set_position retrieve_key stop_movements";
    }
}

sub VeluxKIX300_Get {
    my ($hash, @a) = @_;
    my $name = $hash->{NAME};

    return "\"get $name\" needs at least one argument" if (@a < 2);

    my $cmd = $a[1];
    if ($cmd eq "status") {
        Log3 $name, 3, "VeluxKIX300 ($name) - Get command: status";
        VeluxKIX300_GetHomeData($hash);
        return undef;
    } else {
        return "Unknown argument $cmd, choose one of status";
    }
}

sub VeluxKIX300_Authenticate {
    my ($hash) = @_;
    my $name = $hash->{NAME};
    my $client_id = $hash->{CLIENT_ID};
    my $client_secret = $hash->{CLIENT_SECRET};
    my $username = $hash->{USERNAME};
    my $password = $hash->{PASSWORD};

    my $url = "https://app.velux-active.com/oauth2/token";
    my $data = "grant_type=password&client_id=$client_id&client_secret=$client_secret&username=$username&password=$password&user_prefix=velux";

    Log3 $name, 3, "VeluxKIX300 ($name) - Authenticating...";

    HttpUtils_NonblockingGet({
        url      => $url,
        timeout  => 10,
        hash     => $hash,
        data     => $data,
        method   => "POST",
        header   => "Content-Type: application/x-www-form-urlencoded",
        callback => \&VeluxKIX300_ParseAuthResponse,
    });
}

sub VeluxKIX300_ParseAuthResponse {
    my ($param, $err, $data) = @_;
    my $hash = $param->{hash};
    my $name = $hash->{NAME};

    if ($err ne "") {
        Log3 $name, 3, "VeluxKIX300 ($name) - Error authenticating: $err";
        return;
    }

    my $json;
    eval {
        $json = decode_json($data);
    };
    if ($@) {
        Log3 $name, 3, "VeluxKIX300 ($name) - Error parsing JSON: $@";
        return;
    }

    $hash->{ACCESS_TOKEN} = $json->{access_token};
    $hash->{REFRESH_TOKEN} = $json->{refresh_token};
    $hash->{EXPIRES_IN} = $json->{expires_in};
    $hash->{TOKEN_EXPIRES} = time() + $json->{expires_in};
    Log3 $name, 3, "VeluxKIX300 ($name) - Access token: $hash->{ACCESS_TOKEN}";

    VeluxKIX300_GetHomeID($hash);
}

sub VeluxKIX300_RefreshToken {
    my ($hash) = @_;
    my $name = $hash->{NAME};
    my $client_id = $hash->{CLIENT_ID};
    my $client_secret = $hash->{CLIENT_SECRET};
    my $refresh_token = $hash->{REFRESH_TOKEN};

    my $url = "https://app.velux-active.com/oauth2/token";
    my $data = "grant_type=refresh_token&client_id=$client_id&client_secret=$client_secret&refresh_token=$refresh_token";

    Log3 $name, 3, "VeluxKIX300 ($name) - Refreshing token...";

    HttpUtils_NonblockingGet({
        url      => $url,
        timeout  => 10,
        hash     => $hash,
        data     => $data,
        method   => "POST",
        header   => "Content-Type: application/x-www-form-urlencoded",
        callback => \&VeluxKIX300_ParseRefreshTokenResponse,
    });
}

sub VeluxKIX300_ParseRefreshTokenResponse {
    my ($param, $err, $data) = @_;
    my $hash = $param->{hash};
    my $name = $hash->{NAME};

    if ($err ne "") {
        Log3 $name, 3, "VeluxKIX300 ($name) - Error refreshing token: $err";
        VeluxKIX300_Authenticate($hash);  # Fallback to re-authentication
        return;
    }

    my $json;
    eval {
        $json = decode_json($data);
    };
    if ($@) {
        Log3 $name, 3, "VeluxKIX300 ($name) - Error parsing JSON: $@";
        VeluxKIX300_Authenticate($hash);  # Fallback to re-authentication
        return;
    }

    $hash->{ACCESS_TOKEN} = $json->{access_token};
    $hash->{REFRESH_TOKEN} = $json->{refresh_token} if $json->{refresh_token};
    $hash->{EXPIRES_IN} = $json->{expires_in};
    $hash->{TOKEN_EXPIRES} = time() + $json->{expires_in};
    Log3 $name, 3, "VeluxKIX300 ($name) - Access token refreshed: $hash->{ACCESS_TOKEN}";
}

sub VeluxKIX300_GetHomeID {
    my ($hash) = @_;
    my $name = $hash->{NAME};
    my $access_token = $hash->{ACCESS_TOKEN};

    return if (!$access_token);

    my $url = "https://app.velux-active.com/api/gethomedata";
    my $data = "access_token=$access_token&gateway_types[]=NXG";

    Log3 $name, 3, "VeluxKIX300 ($name) - Getting Home ID...";

    HttpUtils_NonblockingGet({
        url      => $url,
        timeout  => 10,
        hash     => $hash,
        data     => $data,
        method   => "POST",
        header   => "Content-Type: application/x-www-form-urlencoded",
        callback => \&VeluxKIX300_ParseHomeID,
    });
}

sub VeluxKIX300_ParseHomeID {
    my ($param, $err, $data) = @_;
    my $hash = $param->{hash};
    my $name = $hash->{NAME};

    if ($err ne "") {
        Log3 $name, 3, "VeluxKIX300 ($name) - Error fetching Home ID: $err";
        return;
    }

    Log3 $name, 5, "VeluxKIX300 ($name) - Home ID response: $data";  # Log the full response for debugging

    my $json;
    eval {
        $json = decode_json($data);
    };
    if ($@) {
        Log3 $name, 3, "VeluxKIX300 ($name) - Error parsing JSON: $@";
        return;
    }

    Log3 $name, 5, "VeluxKIX300 ($name) - Parsed JSON: " . encode_json($json);  # Log the parsed JSON for debugging

    if (defined($json->{body}->{homes}) && @{$json->{body}->{homes}} > 0) {
        $hash->{HOME_ID} = $json->{body}->{homes}[0]->{id};
        Log3 $name, 3, "VeluxKIX300 ($name) - Home ID: $hash->{HOME_ID}";
        VeluxKIX300_GetHomeData($hash);
    } else {
        Log3 $name, 3, "VeluxKIX300 ($name) - No homes found.";
    }
}

sub VeluxKIX300_GetHomeData {
    my ($hash) = @_;
    my $name = $hash->{NAME};

    if (time() >= $hash->{TOKEN_EXPIRES}) {
        VeluxKIX300_RefreshToken($hash);
        return;
    }

    my $access_token = $hash->{ACCESS_TOKEN};
    my $home_id = $hash->{HOME_ID};

    return if (!$access_token || !$home_id);

    my $url = "https://app.velux-active.com/api/homestatus";
    my $data = "access_token=$access_token&home_id=$home_id";

    Log3 $name, 4, "VeluxKIX300 ($name) - Getting Home Data...";

    HttpUtils_NonblockingGet({
        url      => $url,
        timeout  => 10,
        hash     => $hash,
        data     => $data,
        method   => "POST",
        header   => "Content-Type: application/x-www-form-urlencoded",
        callback => \&VeluxKIX300_ParseHomeData,
    });

    InternalTimer(gettimeofday() + $hash->{INTERVAL}, "VeluxKIX300_GetHomeData", $hash, 0);
}

sub VeluxKIX300_ParseHomeData {
    my ($param, $err, $data) = @_;
    my $hash = $param->{hash};
    my $name = $hash->{NAME};

    if ($err ne "") {
        Log3 $name, 3, "VeluxKIX300 ($name) - Error fetching Home Data: $err";
        return;
    }

    Log3 $name, 5, "VeluxKIX300 ($name) - Home Data response: $data";  # Log the full response for debugging

    my $json;
    eval {
        $json = decode_json($data);
    };
    if ($@) {
        Log3 $name, 3, "VeluxKIX300 ($name) - Error parsing JSON: $@";
        return;
    }

    Log3 $name, 5, "VeluxKIX300 ($name) - Parsed JSON: " . encode_json($json);  # Log the parsed JSON for debugging

    if (defined($json->{body}->{home}) && defined($json->{body}->{home}->{modules})) {
        my @modules = @{$json->{body}->{home}->{modules}};
        readingsBeginUpdate($hash);
        foreach my $module (@modules) {
            my $id = $module->{id};
            my $bridge = $module->{bridge};  # Extract bridge ID for control commands
            $hash->{MODULES}->{$id} = $bridge;  # Store bridge ID in the hash for later use
            Log3 $name, 3, "VeluxKIX300 ($name) - Found module ID: $id with bridge ID: $bridge";  # Log module and bridge IDs for debugging
            while (my ($key, $value) = each %$module) {
                readingsBulkUpdate($hash, "module_${id}_$key", $value);
            }
        }
        readingsEndUpdate($hash, 1);
        Log3 $name, 3, "VeluxKIX300 ($name) - Modules updated.";
    } else {
        Log3 $name, 3, "VeluxKIX300 ($name) - No modules found.";
    }
}

sub VeluxKIX300_ControlDevice {
    my ($hash, $cmd, $device, $position) = @_;
    my $name = $hash->{NAME};

    if (time() >= $hash->{TOKEN_EXPIRES}) {
        VeluxKIX300_RefreshToken($hash);
        return;
    }

    my $access_token = $hash->{ACCESS_TOKEN};
    my $home_id = $hash->{HOME_ID};
    my $bridge = $hash->{MODULES}->{$device};  # Retrieve the bridge ID for the device

    return if (!$access_token || !$home_id || !$bridge);

    my $url = "https://app.velux-active.com/syncapi/v1/setstate";
    my $data;

    if ($cmd eq "set_position") {
        $data = encode_json({
            home => {
                id => $home_id,
                modules => [
                    {
                        id => $device,
                        bridge => $bridge,
                        target_position => int($position),  # Ensure target_position is a number
                    }
                ]
            }
        });
    } elsif ($cmd eq "retrieve_key") {
        $data = encode_json({
            home => {
                id => $home_id,
                modules => [
                    {
                        id => $device,
                        bridge => $bridge,
                        retrieve_key => \1,
                    }
                ]
            }
        });
    } elsif ($cmd eq "stop_movements") {
        $data = encode_json({
            home => {
                id => $home_id,
                modules => [
                    {
                        id => $bridge,  # Use the bridge ID for stopping movements
                        stop_movements => "all",
                    }
                ]
            }
        });
    } else {
        my $action = ($cmd eq "open") ? 100 : 0;
        $data = encode_json({
            home => {
                id => $home_id,
                modules => [
                    {
                        id => $device,
                        bridge => $bridge,
                        target_position => $action,
                    }
                ]
            }
        });
    }

    Log3 $name, 3, "VeluxKIX300 ($name) - Sending command $cmd to module $device with home_id $home_id and bridge_id $bridge...";

    HttpUtils_NonblockingGet({
        url      => $url,
        timeout  => 10,
        hash     => $hash,
        data     => $data,
        method   => "POST",
        header   => "Authorization: Bearer $access_token\r\nContent-Type: application/json",
        callback => \&VeluxKIX300_ParseControlResponse,
    });
}

sub VeluxKIX300_ParseControlResponse {
    my ($param, $err, $data) = @_;
    my $hash = $param->{hash};
    my $name = $hash->{NAME};

    if ($err ne "") {
        Log3 $name, 3, "VeluxKIX300 ($name) - Error controlling module: $err";
        return;
    }

    Log3 $name, 3, "VeluxKIX300 ($name) - Control response: $data";
}

1;

=pod
=item summary    This module integrates Velux KIX300 gateway into FHEM
=item summary_DE Dieses Modul integriert das Velux KIX300 Gateway in FHEM
=begin html

=end html
=cut
FHEM im Proxmox Container