Aiper Irrisense 2

Hallo Leute

Hat sich schon mal jemand mit einem Gerät von Aiper auseinandergesetzt? Ich hab mir den Irrisense 2 geholt. Funktioniert gut. Lediglich der Cloudzwang ist nervig, aber ok.

In den letzten Tagen hab ich mich viel damit beschäftigt und auch den Support bemüht, welcher auch am Sonntag geantwortet hat. Leider ist es so, dass das Teil und auch die AWS Anbindung nicht öffentlich dokumentiert ist. Ich habe GitHub gewälzt, ChatGPT gefragt. Alles ohne Erfolg. Die KI hat zwar ein PHP Skript ausgepsuckt, welches theoretisch funktioniert, aber die Anmeldung klappt nicht. Der Support meint, es gibt nichts. Was auch geht, ist eine BT Verbindung. Wenn man mit der App und dem dazugehörigen Device in der Nähe ist, verbinden sich die beiden über BT. Könnte auch eine Möglichkeit sein.

Selbst die hochheilige HA Kommunity hat da noch nichts. Gibt zwar ein GitHub Projekt, hab ich aber in HA nicht zum Laufen gebracht und wäre auch nur für einen Poolroboter gewesen. Wollte es einfach probieren.

Hab auch versucht, den Traffikt mit dem mitm Proxy zu sniffen, das scheitert am Zertifikat.

Verbaut dürften sie ein ESP Board haben, da sich das Device als “Espressif” bei mir im Netzwerk meldet. Oder ist es nur das WLAN Modul, kann auch sein.

Also, man sieht, ich hätte mich bemüht, bevor ich hier nachfrage :wink: .

Ich hab eine Lösung dafür gefunden. Gibt eine HA Integration über HACS. Funktioniert perfekt und ist schon bei mir in IPS drinnen :smiley:

Weißt du zufällig ob damit auch ein Poolroboter von Aiper damit funktioniert? Wie hast du das in Symcon eingebunden?

VG
Stefan

Mit dem eher nicht. Aber es gibt zwei Projekete für Roboter

Ich habe mir da selber ein paar Hilfsfunktionen für die API geschrieben. Um die Stati zurück zu bekommen, ist in HA eine Automation definiert, die bei Änderung einer Entität einen Webhook von IPS aufruft. Dieser Hook startet ein Skript zum aktualisieren der Variablen. Aber das ganze sollte auch mit dem Modul für HA funktionieren.

Guten Morgen,
danke für die Antwort. Dann wird das wohl nichts, den ich habe kein HA installiert.

VG
Stefan

Ich habe ChatGPT beauftragt, das Projekt zu analysieren und es ist das herausgekommen

<?php

/* -----------------------------------------------------------
   Aiper IrriSense 2 für IP-Symcon
   basierend auf Analyse ha-aiper-irrisense-2
------------------------------------------------------------ */

class AiperIrriSense
{
    private $email;
    private $password;
    private $baseUrl = 'https://apieurope.aiper.com';

    private $token = '';
    private $tokenExpire = 0;

    private $requestIdKey = 'K6!R]y_]Q!gA,5vy';

    private $publicKey = <<<KEY
-----BEGIN PUBLIC KEY-----
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCIKoKPqwq1f60hm/2lpHDF/DT4
J9YaptuTq78nsxdgnSBAvkIZ3E8dq bEBT/VETjJ9Yr28QtHX13E8QGByYxLzYPld
HNXChgOWfSemTEC3TxPvlaSuM9eFUuhqSeGbgoKG7JJNlgjvsPO2cHEhPXJE4qWt
KEZVOZBxEeCgAaLZxwIDAQAB
-----END PUBLIC KEY-----
KEY;

    public function __construct($email, $password)
    {
        $this->email = $email;
        $this->password = $password;
    }

    /* -------------------------------------------------- */
    /* LOGIN */
    /* -------------------------------------------------- */

    public function Login()
    {
        $data = [
            'email'    => $this->email,
            'password' => $this->password
        ];

        $res = $this->Call('/login', $data, false);

        if (!isset($res['data']['token'])) {
            throw new Exception("Login fehlgeschlagen");
        }

        $this->token = $res['data']['token'];
        $this->tokenExpire = $res['data']['tokenExpires'];

        if (!empty($res['data']['domain'][0])) {
            $this->baseUrl = rtrim($res['data']['domain'][0], '/');
        }

        return true;
    }

    private function EnsureLogin()
    {
        if (!$this->token || time() > ($this->tokenExpire - 300)) {
            $this->Login();
        }
    }

    /* -------------------------------------------------- */
    /* GERÄTE */
    /* -------------------------------------------------- */

    public function GetDevices()
    {
        $this->EnsureLogin();
        return $this->Call('/equipment/getEquipmentList', []);
    }

    public function GetEquipmentInfo($sn)
    {
        $this->EnsureLogin();
        return $this->Call('/equipment/getEquipmentInfo', [
            'sn' => $sn
        ]);
    }

    public function CheckOnline($sn)
    {
        $this->EnsureLogin();
        return $this->Call('/equipment/checkEquipmentOnlineStatus', [
            'sn' => $sn
        ]);
    }

    /* -------------------------------------------------- */
    /* IRRISENSE FUNKTIONEN */
    /* -------------------------------------------------- */

    public function GetZoneMap($sn)
    {
        $this->EnsureLogin();

        return $this->Call('/wr/getMapList', [
            'sn' => $sn
        ]);
    }

    public function StartZone($sn, $mapId, $waterYield = 0.1)
    {
        $this->EnsureLogin();

        return $this->Call('/wr/setWorkMode', [
            'sn'         => $sn,
            'mode'       => 1,
            'map_id'     => intval($mapId),
            'status'     => 1,
            'waterYield' => $waterYield
        ]);
    }

    public function StopZone($sn, $mapId)
    {
        $this->EnsureLogin();

        return $this->Call('/wr/setWorkMode', [
            'sn'     => $sn,
            'mode'   => 0,
            'map_id' => intval($mapId),
            'status' => 0
        ]);
    }

    public function GetHistory($sn)
    {
        $this->EnsureLogin();

        return $this->Call('/wr/getWorkHistory', [
            'sn' => $sn
        ]);
    }

    /* -------------------------------------------------- */
    /* API CALL */
    /* -------------------------------------------------- */

    private function Call($path, $body = [], $useToken = true)
    {
        $url = $this->baseUrl . $path;

        $json = json_encode($body, JSON_UNESCAPED_UNICODE);
        $post = $this->EncryptPayload($json);

        $headers = [
            'Content-Type: application/json',
            'requestidkey: ' . $this->requestIdKey,
            'appversion: 3.3.0',
            'appos: ios'
        ];

        if ($useToken && $this->token) {
            $headers[] = 'token: ' . $this->token;
        }

        $ch = curl_init($url);

        curl_setopt_array($ch, [
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_POST => true,
            CURLOPT_POSTFIELDS => $post,
            CURLOPT_HTTPHEADER => $headers,
            CURLOPT_TIMEOUT => 30
        ]);

        $response = curl_exec($ch);
        curl_close($ch);

        if (!$response) {
            throw new Exception("Keine Antwort");
        }

        return json_decode($response, true);
    }

    /* -------------------------------------------------- */
    /* APP ENCRYPTION (vereinfacht) */
    /* -------------------------------------------------- */

    private function EncryptPayload($json)
    {
        $key = substr(md5(mt_rand()), 0, 16);
        $iv  = substr(md5(mt_rand()), 0, 16);

        $encrypted = openssl_encrypt(
            $json,
            'AES-128-CBC',
            $key,
            OPENSSL_RAW_DATA,
            $iv
        );

        $data = base64_encode($encrypted);

        $keyData = json_encode([
            'key' => $key,
            'iv'  => $iv
        ]);

        openssl_public_encrypt($keyData, $rsa, $this->publicKey);
        $encKey = base64_encode($rsa);

        return json_encode([
            'data' => $data,
            'encryptKey' => $encKey
        ]);
    }
}

/* -----------------------------------------------------------
   BEISPIEL
------------------------------------------------------------ */

$aiper = new AiperIrriSense("MAIL@DOMAIN.TLD", "PASSWORT");

/* Login */
$aiper->Login();

/* Geräte holen */
$devices = $aiper->GetDevices();
print_r($devices);

/* Seriennummer einsetzen */
$sn = "WRX123456789";

/* Zone starten */
#$aiper->StartZone($sn, 1);

/* Zone stoppen */
#$aiper->StopZone($sn, 1);

/* Status */
#print_r($aiper->GetEquipmentInfo($sn));

?>

Ich selbst habe es noch nicht ausprobiert.

Noch ein paar Kommentare von der KI

Was bereits funktioniert

Cloud API

  • Login

  • Token

  • Geräte

  • Gerätestatus

  • Onlinecheck

  • Historie

Bewässerung

  • Zone starten

  • Zone stoppen


Was noch offen ist

MQTT Live Status

Wenn du Echtzeitwerte willst:

  • aktuell laufende Minute

  • Fortschritt

  • Push Status

  • Fehler sofort

Meine Empfehlung für IP-Symcon

  1. Dieses Skript als zyklisches Script alle 2 Minuten

  2. Variablen setzen:

    • Online

    • Running

    • Active Zone

    • Last Watering

  3. Separate Aktionen Start/Stop

Eventuell kannst du damit was anfangen. Ich werde es mal versuchen. Dauert aber.

Wobei du ja den Roboter haben willst :wink: Das Projekt basiert aber auf diesem GitHub - kmich/ha-aiper: Home Assistant integration for Aiper pool cleaners (Scuba X1) — unofficial community project · GitHub . Du könntest dir das selbe mit der KI erstellen lassen. Vielleicht wirds ja was.

Ich habe auch gerade rausgefunden das mein S1 das ganze unterstützt:

deviceModel: Scuba_S1_2025
name: Scuba S1 2025
sn: 52XXXXXXXXX
battLevel: 100
zoneId: Europe/Berlin
online: 0

und das Symcon später folgendes abrufen könnte:

  • Batterie
  • Online-Status
  • Gerätestatus
  • Start / Pause / Stop
  • ggf. Reinigungsmodus

Verfügbare Werte:

Online: ja
Batterie: 100 %
Status: machineStatus = 0
Laufzeit: 0
WLAN RSSI: -58
IP: 192.168.40.213
Firmware: V1.0.1
SN: 52X52800381

Damit funktioniert das Login. Leider ist mein freies Kontingent aufgebraucht und ich muss 4,5 Stunden warten :wink:
Devicelist funktioniert noch nicht.

    <?php

    class AiperIrriSense
    {
        private $email;
        private $password;

        private $baseUrl = 'https://apieurope.aiper.com';

        private $token = '';
        private $tokenExpire = 0;

        private $lastKey = '';
        private $lastIv  = '';
        
        public function __construct($email, $password)
        {
            $this->email = $email;
            $this->password = $password;
        }

        /* ---------------------------------------------------- */
        /* LOGIN */
        /* ---------------------------------------------------- */

        public function Login()
        {
            $res = $this->Call('/login', [
                'email'    => $this->email,
                'password' => $this->password
            ], false);

            if (!isset($res['code']) || $res['code'] != 200) {
                throw new Exception('Login fehlgeschlagen');
            }

            $this->token = $res['data']['token'];
            $this->tokenExpire = time() + intval($res['data']['tokenExpires']);

            if (!empty($res['data']['domain'][0])) {
                $this->baseUrl = rtrim($res['data']['domain'][0], '/');
            }

            return true;
        }

        private function EnsureLogin()
        {
            if (!$this->token || time() > ($this->tokenExpire - 300)) {
                $this->Login();
            }
        }

        /* ---------------------------------------------------- */
        /* GERÄTE */
        /* ---------------------------------------------------- */

        public function GetDevices()
        {
            $this->EnsureLogin();

            return $this->Call('/equipment/getEquipmentList', []);
        }

        public function GetEquipmentInfo($sn)
        {
            $this->EnsureLogin();

            return $this->Call('/equipment/getEquipmentInfo', [
                'sn' => $sn
            ]);
        }

        public function CheckOnline($sn)
        {
            $this->EnsureLogin();

            return $this->Call('/equipment/checkEquipmentOnlineStatus', [
                'sn' => $sn
            ]);
        }

        /* ---------------------------------------------------- */
        /* IRRISENSE */
        /* ---------------------------------------------------- */

        public function GetZoneMap($sn)
        {
            $this->EnsureLogin();

            return $this->Call('/wr/getMapList', [
                'sn' => $sn
            ]);
        }

        public function StartZone($sn, $mapId, $waterYield = 0.1)
        {
            $this->EnsureLogin();

            return $this->Call('/wr/setWorkMode', [
                'sn'         => $sn,
                'mode'       => 1,
                'map_id'     => intval($mapId),
                'status'     => 1,
                'waterYield' => $waterYield
            ]);
        }

        public function StopZone($sn, $mapId)
        {
            $this->EnsureLogin();

            return $this->Call('/wr/setWorkMode', [
                'sn'     => $sn,
                'mode'   => 0,
                'map_id' => intval($mapId),
                'status' => 0
            ]);
        }

        /* ---------------------------------------------------- */
        /* API CALL */
        /* ---------------------------------------------------- */

        private function Call($path, $body = [], $useToken = true)
        {
            $enc = $this->EncryptPayload($body);

            $headers = [
                'Content-Type: application/json',
                'version: 3.3.0',
                'os: ios',
                'platform: ios',
                'appType: aiper',
                'language: en',
                'charset: UTF-8',
                'requestidkey: K6!R]y_]Q!gA,5vy',
                'encryptKey: '.$enc['header']
            ];

            if ($useToken && $this->token) {
                $headers[] = 'token: ' . $this->token;
            } else {
                $headers[] = 'token:';
            }

            $ch = curl_init($this->baseUrl . $path);

            curl_setopt_array($ch, [
                CURLOPT_RETURNTRANSFER => true,
                CURLOPT_POST => true,
                CURLOPT_POSTFIELDS => $enc['body'],
                CURLOPT_HTTPHEADER => $headers,
                CURLOPT_TIMEOUT => 30
            ]);

            $response = curl_exec($ch);

            if ($response === false) {
                throw new Exception(curl_error($ch));
            }
            $http = curl_getinfo($ch, CURLINFO_HTTP_CODE);
            $type = curl_getinfo($ch, CURLINFO_CONTENT_TYPE);

curl_close($ch);

$decoded = $this->DecryptResponse($response);

echo "PATH: ".$path.PHP_EOL;
echo "RAW:".PHP_EOL;
var_dump($response);

echo PHP_EOL."DECRYPTED:".PHP_EOL;
var_dump($decoded);

echo PHP_EOL."JSON ERROR: ".json_last_error_msg().PHP_EOL;

die();
        }

        private function DecryptResponse($cipherText)
        {
            $raw = base64_decode($cipherText);

            $plain = openssl_decrypt(
                $raw,
                'AES-128-CBC',
                $this->lastKey,
                OPENSSL_RAW_DATA | OPENSSL_ZERO_PADDING,
                $this->lastIv
            );

            $plain = rtrim($plain, "\0");

            return $plain;
        }

        /* ---------------------------------------------------- */
        /* ENCRYPTION */
        /* ---------------------------------------------------- */

        private function EncryptPayload($body)
        {
            $chars = '';

            for ($i = 40; $i <= 126; $i++) {
                $chars .= chr($i);
            }

            $key = '';
            $iv  = '';

            for ($i = 0; $i < 16; $i++) {
                $key .= $chars[random_int(0, strlen($chars) - 1)];
                $iv  .= $chars[random_int(0, strlen($chars) - 1)];
            }

            $payload = [
                'timestamp' => round(microtime(true) * 1000),
                'nonce'     => substr(md5(mt_rand()),0,4)
                ];

            foreach($body as $k=>$v){
                $payload[$k]=$v;
            }

            $json = json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);

            $pad = 16 - (strlen($json) % 16);

            if ($pad < 16) {
                $json .= str_repeat(chr(0), $pad);
            }

            $encrypted = openssl_encrypt(
                $json,
                'AES-128-CBC',
                $key,
                OPENSSL_RAW_DATA | OPENSSL_ZERO_PADDING,
                $iv
            );

            $pubKeyPem = <<<KEY
            -----BEGIN PUBLIC KEY-----
            MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCIKoKPqwq1f60hm/2lpHDF/DT4
            J9YaptuTq78nsxdgnSBAvkIZ3E8dqbEBT/VETjJ9Yr28QtHX13E8QGByYxLzYPld
            HNXChgOWfSemTEC3TxPvlaSuM9eFUuhqSeGbgoKG7JJNlgjvsPO2cHEhPXJE4qWt
            KEZVOZBxEeCgAaLZxwIDAQAB
            -----END PUBLIC KEY-----
            KEY;

            $pubKey = openssl_pkey_get_public($pubKeyPem);
            if ($pubKey === false) {
                throw new Exception('Public Key konnte nicht geladen werden');
            }

            $keyData = json_encode([
                'key' => $key,
                'iv'  => $iv
            ], JSON_UNESCAPED_SLASHES);
            
            $this->lastKey = $key;
            $this->lastIv  = $iv;

            openssl_public_encrypt(
                $keyData,
                $rsaEncrypted,
                $pubKey,
                OPENSSL_PKCS1_PADDING
            );

            return [
                'header' => base64_encode($rsaEncrypted),
                'body'   => json_encode([
                    'data' => base64_encode($encrypted)
                ])
            ];
        }
    }

    /* ---------------------------------------------------- */
    /* AUSFÜHRUNG */
    /* ---------------------------------------------------- */

    $aiper = new AiperIrriSense(
        'MAILADRESSE',
        'PASSWORT'
    );

    $aiper->Login();
    print_r($aiper->GetDevices());

Ich habe es jetzt so gemacht das ich in Proxmox einen LXC mit Debian erstellt habe und das Ganze läuft über Python und funktioniert. Die Daten werden an Symcon übermittelt. Start, Stop und Pause muss ich noch probieren.

Funktioniert bei euch die Aiper App? Der Logon. Bei mir funktioniert weder die App, noch die HA integration. In der App bekomme ich “Aktuelles Netzwerk nicht gefunden”. Selber Fehler mit WLAN und Mobilverbindung.