FHEM-Integration von Tapo Kameras

Begonnen von Prof. Dr. Peter Henning, 18 März 2026, 11:39:03

Vorheriges Thema - Nächstes Thema

DeeSPe

Zitat von: Prof. Dr. Peter Henning am 19 März 2026, 17:06:22Nochmal, es gibt m.e. 2 Möglichkeiten

a.) Username und passwort des _in der kamera_ eingerichteten kontos
b.) admin und cloud-passwort für das Tapo-System

Beide jetzt mehrfach probiert! Computer sagt: nein!

Beim Aufrufen der tapo_control.py bekomme ich folgenden Fehler:
root@fhem-test:/opt/fhem/tapo# ./tapo_control.py
  File "/opt/fhem/tapo/./tapo_control.py", line 27
    try:def safe_call(label, func):
        ^^^
SyntaxError: invalid syntax

Gruß
Dan
MAINTAINER: 22_HOMEMODE, 98_Hyperion, 98_FileLogConvert, 98_serviced

Als kleine Unterstützung für meine Programmierungen könnt ihr mir gerne einen Kaffee spendieren: https://buymeacoff.ee/DeeSPe

Prof. Dr. Peter Henning

#16
Zitat von: DeeSPe am 19 März 2026, 18:53:15Beim Aufrufen der tapo_control.py bekomme ich folgenden Fehler:
Vlt. ein Tippfehler beim Übertragen ins Wiki. Mal die angehängte Datei nehmen. Nutzt aber nichts, wenn die credentials zurückgewiesen werden.

Edit: Wichtig: In der Tapo-App unter "Ich" unbedingt "Dienste von Drittanbietern" aufrufen und "Kompatibilität mit Drittanbietern" auf EIN stellen !
Ich habe kurzerhand vergessen, das ins Wiki zu schreiben.

LG

pah

DeeSPe

#17
Zitat von: Prof. Dr. Peter Henning am 19 März 2026, 19:41:39Edit: Wichtig: In der Tapo-App unter "Ich" unbedingt "Dienste von Drittanbietern" aufrufen und "Kompatibilität mit Drittanbietern" auf EIN stellen !

Habe ich jetzt aktiviert, bringt aber leider nicht den erhofften Erfolg.
Folgenden Kombinationen aus Logindaten habe ich NICHT erfolgreich getestet:
admin:<CLOUD-PW>
<CLOUD-ANMELDENAME>:<CLOUD-PW>
<KAMERAKONTO-NAME>:<KAMERAKONTO-PW>

Aktuelle C222 Firmware v1.4.1 und keine neuere verfügbar.

Gruß
Dan

P.S. Hab mal ein paar kleine Berichtigungen im Wiki gemacht.
MAINTAINER: 22_HOMEMODE, 98_Hyperion, 98_FileLogConvert, 98_serviced

Als kleine Unterstützung für meine Programmierungen könnt ihr mir gerne einen Kaffee spendieren: https://buymeacoff.ee/DeeSPe

DeeSPe

#18
Keine Ahnung warum, aber jetzt, nach Ablauf der Anmeldesperre, hat es endlich geklappt mit admin:<CLOUD-PW>

root@fhem-test:/opt/fhem/tapo# ./tapo_testmethod.py
getAlarm
getAlarmConfig
getAudioConfig
getAudioSpec
getChimeAlarmConfigure
getChimeRingPlan
getFloodlightCapability
getFloodlightConfig
getFloodlightStatus
getHubSirenConfig
getHubSirenStatus
getHubSirenTypeList
getLightFrequencyMode
getPrivacyMode
getRingStatus
getSupportAlarmTypeList
manualFloodlightOp
playAlarm
setAlarm
setChimeAlarmConfigure
setChimeRingPlan
setFloodlightConfig
setHubSirenConfig
setHubSirenStatus
setLightFrequencyMode
setMicrophone
setPrivacyMode
setRecordAudio
setRingStatus
setSirenStatus
setSpeakerVolume
startManualAlarm
stopManualAlarm
testUsrDefAudio

Gruß
Dan

EDIT: tapo_control.py funktioniert auch, ist nur sehr träge.
MAINTAINER: 22_HOMEMODE, 98_Hyperion, 98_FileLogConvert, 98_serviced

Als kleine Unterstützung für meine Programmierungen könnt ihr mir gerne einen Kaffee spendieren: https://buymeacoff.ee/DeeSPe

Prof. Dr. Peter Henning

#19
So, ich habe das jetzt auf einem guten Stand. Mit einem FHEM Dummy
defmod TapoCam dummy
attr TapoCam readingList snapshot motor_action motor_result motor_presets error
attr TapoCam setList takePhoto:noArg privacy:on,off light:on,off light_intensity light_duration light_night:ir,white,auto led:on,off left right up down calibrate:noArg preset_goto:1,2,3,4,5,6,7,8 preset_save preset_delete presets_get detection_motion detection_person detection_pet detection_tamper detection_vehicle detection_linecrossing alarm:on,off alarm_light:on,off alarm_sound:on,off alarm_duration alarm_volume:low,medium,high
attr TapoCam webCmd takePhoto:left:right:up:down
und einem DOIF
defmod TapoCam.N DOIF ([TapoCam:state] ne "ready") \
({TapoCamHandler("$DEVICE","$EVENT")})\
(setreading TapoCam state ready)\
\
DOELSEIF\
([+00:05:00])\
({TapoCamHandler("TapoCam","status_update")})\
\

attr TapoCam.N do always
attr TapoCam.N group Control
attr TapoCam.N room Kontrollraum
attr TapoCam.N wait 0,3
sowie zwei verschiedenen Perl-Funktionen
sub TapoCamHandler($$){
  my ($name,$event) = @_;
 
  my $hash = $defs{$name};
  return if(!$hash);
  my $res;
  my $val;
  my $cmd;
 
  #--
  if( $event =~ /^takePhoto$/){
    $res = qx(/opt/fhem/tapo/tapo_snapshot.sh);
    chomp($res);
    fhem("setreading $name snapshot $res");
 
  #-- status update
  }elsif( $event eq "status_update" ){
    my $cmd = '/opt/fhem/tapo/.venv/bin/python3 /opt/fhem/tapo/tapo_control_privacy.py; '
          . '/opt/fhem/tapo/.venv/bin/python3 /opt/fhem/tapo/tapo_control_light.py status; '
          . '/opt/fhem/tapo/.venv/bin/python3 /opt/fhem/tapo/tapo_control_alarm.py status; '
          . '/opt/fhem/tapo/.venv/bin/python3 /opt/fhem/tapo/tapo_control_detection.py status; '
          . '/opt/fhem/tapo/.venv/bin/python3 /opt/fhem/tapo/tapo_control_events.py events 300; ';
    system('/bin/sh', '-c', "($cmd) >/dev/null 2>&1 &");
  
  #-- privacy
  }elsif( $event =~ /^privacy\s+(on|off)$/){
    $event = $1;
    system("/opt/fhem/tapo/.venv/bin/python3 /opt/fhem/tapo/tapo_control_privacy.py $event >/dev/null 2>&1 &");
   
  #-- light
  }elsif($event =~ /^(led|light)\s+(on|off)$/){
    $event = $1;
    $val = $2;
    system("/opt/fhem/tapo/.venv/bin/python3 /opt/fhem/tapo/tapo_control_light.py $event $val >/dev/null 2>&1 &");
  }elsif($event =~ /^light_(intensity|duration)\s+(\d+)$/){
    $event = $1;
    $val = $2;
    $event =~ s/duration/time/;
    system("/opt/fhem/tapo/.venv/bin/python3 /opt/fhem/tapo/tapo_control_light.py $event $val >/dev/null 2>&1 &");
  }elsif($event =~ /^light_night\s+(ir|white|auto)$/){
    $event ="night";
    $val = $1;
    system("/opt/fhem/tapo/.venv/bin/python3 /opt/fhem/tapo/tapo_control_light.py $event $val >/dev/null 2>&1 &");
   
  #-- motor
  }elsif( $event =~ /^(left|right|up|down|calibrate|presets_get)$/){
    $event =~ s/_get//;
    system("/opt/fhem/tapo/.venv/bin/python3 /opt/fhem/tapo/tapo_control_motor.py $event >/dev/null 2>&1 &");
  }elsif( $event =~ /^(left|right|up|down|preset_goto|preset_delete)\s+(\d+)$/){
    $event = $1;
    $val = $2;
    $event =~ s/preset_//;
    system("/opt/fhem/tapo/.venv/bin/python3 /opt/fhem/tapo/tapo_control_motor.py $event $val >/dev/null 2>&1 &");
  }elsif( $event =~ /^preset_save\s+(\d+)\s+(.+)$/){
    $event = "save";
    $val = $1;
    $cmd = $2;
    system("/opt/fhem/tapo/.venv/bin/python3 /opt/fhem/tapo/tapo_control_motor.py $event $val $cmd >/dev/null 2>&1 &");
 
  #-- detection
  }elsif( $event =~ /^detection_(motion|person|pet|tamper|vehicle|linecrossing)\s+(\d+)$/){
    $event = $1;
    $val = $2;
    system("/opt/fhem/tapo/.venv/bin/python3 /opt/fhem/tapo/tapo_control_detection.py $event $val >/dev/null 2>&1 &");
 
  #-- alarm
   }elsif($event =~ /^alarm\s+(on|off)$/){
    $val = $1;
    system("/opt/fhem/tapo/.venv/bin/python3 /opt/fhem/tapo/tapo_control_alarm.py $val >/dev/null 2>&1 &");
  }elsif( $event =~ /^alarm_(light|sound)\s+(on|off)$/){
    $event = $1;
    $val = $2;
    system("/opt/fhem/tapo/.venv/bin/python3 /opt/fhem/tapo/tapo_control_alarm.py $event $val >/dev/null 2>&1 &");
  }elsif( $event =~ /^alarm_(volume|duration)\s+(.+)$/){
    $event = $1;
    $val = $2;
    system("/opt/fhem/tapo/.venv/bin/python3 /opt/fhem/tapo/tapo_control_alarm.py $event $val >/dev/null 2>&1 &");
  }
}
zum Setzen und
sub TapoReturnHandler($$$){
  my ($name,$group,$json) = @_;
 
  my $hash = $defs{$name};
  return if(!$hash);
 
  #Log 1,"obtained return to $name from Tapo group $group. json-result = $json";
 
  #-- JSON-String nach Perl-Hash wandeln
  if(!defined($json) || $json eq "") {
    readingsSingleUpdate($hash, "error", "empty json", 1) if $hash;
    return;
  }
  my $data;
  eval {
    $data = decode_json($json);
  };
  if($@ || ref($data) ne "HASH") {
    readingsSingleUpdate($hash, "error", "invalid json for group $group", 1);
    return;
  }
 
  #-- privacy
  if( $group eq "privacy" ){
    readingsBeginUpdate($hash);
    #-- Fehler-Rückgabe aus Python
    if(defined($data->{result})
      && $data->{result} eq "error"){
      my $msg = $data->{message} // "unknown error";
      readingsBulkUpdate($hash, "error", $msg);
      readingsBulkUpdate($hash, "state", "$group error: $msg");
    }else {
      my $state = $data->{privacy} // "";
      readingsBulkUpdate($hash,"privacy",$state);
      readingsBulkUpdate($hash, "error", "");
      readingsBulkUpdate($hash, "state", "privacy $state");
    }
    readingsEndUpdate($hash,1);
   
  #-- light
  }elsif($group eq "light") {
    readingsBeginUpdate($hash);
    #-- Fehler-Rückgabe aus Python
    if(defined($data->{result})
      && $data->{result} eq "error"){
      my $msg = $data->{message} // "unknown error";
      readingsBulkUpdate($hash, "error", $msg);
      readingsBulkUpdate($hash, "state", "$group error: $msg");
    }else {
      readingsBulkUpdate($hash, "light",           $data->{status})      if defined $data->{status};
      readingsBulkUpdate($hash, "light_intensity", $data->{intensity})   if defined $data->{intensity};
      readingsBulkUpdate($hash, "light_duration",  $data->{time})        if defined $data->{time};
      readingsBulkUpdate($hash, "light_remain",    $data->{time_remain}) if defined $data->{time_remain};
      readingsBulkUpdate($hash, "light_night",     $data->{night})       if defined $data->{night};
      readingsBulkUpdate($hash, "led",             $data->{led})         if defined $data->{led};
      readingsBulkUpdate($hash, "error", "");
      readingsBulkUpdate($hash, "state", "light ok");
    }
    readingsEndUpdate($hash, 1);
    
  #-- motor
  }elsif( $group eq "motor" ){
    readingsBeginUpdate($hash);
    #-- Fehler-Rückgabe aus Python
    if(defined($data->{result}) && $data->{result} eq "error") {
      my $msg = $data->{message} // "unknown error";
      readingsBulkUpdate($hash, "error", $msg);
      readingsBulkUpdate($hash, "state", "$group error: $msg");
    }else{
      my $action = $data->{action} // "";
      my $result = $data->{result} // "";
      my $motor_action = "";
      my $state = "";

      #-- motor_action zusammensetzen
      if($action =~ /^(left|right|up|down)$/) {
        my $value = $data->{value} // 0;
        $motor_action = "$action $value";
      }elsif($action =~ /^(goto|save|delete)$/) {
        my $preset = $data->{preset} // "";
        $motor_action = "$action $preset";
      }
      else {
        $motor_action = $action;
      }
      readingsBulkUpdate($hash, "motor_action", $motor_action);
      readingsBulkUpdate($hash, "motor_result", $result);
      $state = $motor_action || $result;

      #-- presets-Liste
      if($action eq "presets") {
        my $presets = $data->{presets};
        if(ref($presets) eq "HASH") {
          my @names = map { $presets->{$_} } sort { $a <=> $b } keys %{$presets};
          my $preset_list = join(",", @names);
          readingsBulkUpdate($hash, "motor_presets", $preset_list);
          $state = "presets: $preset_list";
        } else {
          readingsBulkUpdate($hash, "motor_presets", "");
          $state = "presets";
        }
      }
      readingsBulkUpdate($hash, "error", "");
      readingsBulkUpdate($hash, "state", $state);
    }
    readingsEndUpdate($hash, 1);
 
  #-- detection
  }elsif($group eq "detection") {
    readingsBeginUpdate($hash);
    #-- Fehler-Rückgabe aus Python
    if(defined($data->{result}) && $data->{result} eq "error") {
      my $msg = $data->{message} // "unknown error";
      readingsBulkUpdate($hash, "error", $msg);
      readingsBulkUpdate($hash, "state", "$group error: $msg");
    }else{
      #-- Fall 1: kompletter Status
      if(ref($data->{motion})       eq "HASH"
        || ref($data->{person})       eq "HASH"
        || ref($data->{vehicle})      eq "HASH"
        || ref($data->{pet})          eq "HASH"
        || ref($data->{tamper})       eq "HASH"
        || ref($data->{linecrossing}) eq "HASH") {
        foreach my $type (qw(motion person vehicle pet tamper linecrossing)) {
          next if ref($data->{$type}) ne "HASH";

          my $enabled = $data->{$type}{enabled};
          next if !defined $enabled;

          my $reading = "detection_$type";
          my $value;

          if($type eq "linecrossing") {
            $value = $enabled;
          } else {
            if($enabled eq "off") {
              $value = "off";
            } else {
              my $sens = $data->{$type}{sensitivity};
              $value = defined($sens) ? "on $sens" : "on";
            }
          }
          readingsBulkUpdate($hash, $reading, $value);
        }
        readingsBulkUpdate($hash, "error", "");
        readingsBulkUpdate($hash, "state", "detection status");

      #-- Fall 2: Rückgabe eines set-Befehls
      }elsif(defined $data->{action}) {
        my $action  = $data->{action} // "";
        my $enabled = $data->{enabled};
        my $value   = $data->{value};
        my $result  = $data->{result};
        my $reading = "detection_$action";
        my $onoff = defined($enabled) ? ($enabled ? "on" : "off") : "";

        if($action eq "linecrossing") {
          readingsBulkUpdate($hash, $reading, $onoff) if $onoff ne "";
        } else {
          my $reading_value;
          if($onoff eq "off") {
            $reading_value = "off";
          } else {
            $reading_value = "on";
            $reading_value .= " $value" if defined $value;
          }
          readingsBulkUpdate($hash, $reading, $reading_value) if $reading_value ne "";
        }
        my $state = $action;
        $state .= " $onoff" if $onoff ne "";
        $state .= " $value" if defined $value && $action ne "linecrossing" && $onoff eq "on";
        readingsBulkUpdate($hash, "error", "");
        readingsBulkUpdate($hash, "state", $state);
      }
    }
    readingsEndUpdate($hash, 1);
   
  #-- alarm
  }elsif($group eq "alarm") {
    readingsBeginUpdate($hash);
    #-- Fehler-Rückgabe aus Python
    if(defined($data->{result})
      && $data->{result} eq "error"){
      my $msg = $data->{message} // "unknown error";
      readingsBulkUpdate($hash, "error", $msg);
      readingsBulkUpdate($hash, "state", "alarm error: $msg");
    }else {
      readingsBulkUpdate($hash, "alarm",          $data->{status})   if defined $data->{status};
      readingsBulkUpdate($hash, "alarm_sound",    $data->{sound})    if defined $data->{sound};
      readingsBulkUpdate($hash, "alarm_light",    $data->{light})    if defined $data->{light};
      readingsBulkUpdate($hash, "alarm_duration", $data->{duration}) if defined $data->{duration};
      readingsBulkUpdate($hash, "alarm_volume",   $data->{volume})   if defined $data->{volume};
     readingsBulkUpdate($hash, "error", "");

      my $state = "";
      $state .= "alarm:$data->{status} " if defined $data->{status};
      $state .= "sound:$data->{sound} " if defined $data->{sound};
      $state .= "light:$data->{light} " if defined $data->{light};
      $state .= "vol:$data->{volume} " if defined $data->{volume};
      $state .= "dur:$data->{duration}" if defined $data->{duration};
      $state =~ s/\s+$//;
      readingsBulkUpdate($hash, "state", $state) if $state ne "";
    }
   readingsEndUpdate($hash, 1);

   #-- events
   }elsif( $group eq "events" ){
    readingsBeginUpdate($hash);
    #-- Fehler-Rückgabe aus Python
    if(defined($data->{result})
      && $data->{result} eq "error"){
      my $msg = $data->{message} // "unknown error";
      readingsBulkUpdate($hash, "error", $msg);
      readingsBulkUpdate($hash, "state", "$group error: $msg");
    }else {
      my $window = $data->{events_window} // "";
      my $start  = $data->{events_start}  // "";
      my $list   = $data->{events_list}   // "";

      readingsBulkUpdate($hash, "events_window", $window);
      readingsBulkUpdate($hash, "events_start",  $start);
      readingsBulkUpdate($hash, "events_list",   $list);
      readingsBulkUpdate($hash, "error", "");
      readingsBulkUpdate($hash, "state", "events updated");
    }
    readingsEndUpdate($hash,1);
  }
}
zum Auswerten der Returns kann ich die Kamera jetzt wunderbar steuern.
Die Python-Programme habe ich alle noch einmal grundlegend überarbeitet. Weil die relativ langsam laufen, erfolgt eine asychrone Rückmeldung an FHEM über einen REST-Call, FHEM wird also nicht blockiert. Alarm etc. lässt sich prima setzen, bei den Events muss man allerdings pollen - die werden über ONVIF nicht ausgegeben. Derzeit werden in FHEM jeweils die Events der letzten 5 Minuten angezeigt.

Jetzt könntet ihr mit dem Zeug erst einmal spielen. Die Doku im Wiki werde ich noch nachziehen, für heute habe ich allerdings genug  :P

LG

pah

Prof. Dr. Peter Henning

Damit ich die Videos auch mit einem Browser ansehen kann, habe ich basierend auf einem älteren Thread
https://forum.fhem.de/index.php?topic=64081.0
auch noch eine Umsetzung auf andere Formate gebaut. Astrein, das Dummy-device kriegt jetzt auch noch ein stateFormat, das bei laufender Kamera einen direkten Link anbietet.

LG

pah

Prof. Dr. Peter Henning

Nächste Ergänzung: Ich kann mir (jedenfalls so lange Platz auf der SD-Karte ist) in meinem FHEM-Device die Events des jeweiligen Tages anschauen. Die werden durchnummeriert, z.B.
Zitat28 10:21:49 2026-03-22 person
29 10:19:52 2026-03-22 person
30 09:39:25 2026-03-22 person
31 09:09:59 2026-03-22 pet
32 09:07:39 2026-03-22 person
33 09:04:51 2026-03-22 pet
34 08:47:17 2026-03-22 pet
35 08:43:59 2026-03-22 pet

Wenn ich dann eingebe "set TapoCam download 34", läuft ein Python-Skript los, prüft die Videos auf der SD-Karte. Lädt dasjenige runter, das automatisch bei diesem Event aufgenommen wurde. Wandelt das mit ffmpeg in eine MP4-Datei um. Und legt mir in das Reading "event_downloadlink" einen Link auf diese MP4-Datei. Diese wird natürlich in den Webspace von FHEMWEB verlinkt, so dass ich sie mit einem Mausklick ansehen kann.

Sieht dann so aus (natürlich habe ich das runterskaliert, die richtig hohe Auflösung wollte ich jetzt nicht posten)...

Scharfe Sache, endlich Katzenüberwachung...

LG

pah




Prof. Dr. Peter Henning

Ich habe jetzt die Dokumentation im FHEM Wiki https://wiki.fhem.de/wiki/Tapo_Kameras komplettiert. Da fehlt zwar noch etwas Prosa, mit der Beschreibung sollte aber auch jetzt schon jeder in der Lage sein, das Teil bei sich zum Laufen zu bekommen.

Die Skripte selber, ebenso wie die Perl-Funktionen zum Handling und das "Snapshot"-Skript habe ich jetzt auf github ausgelagert, Adresse siehe Wiki.

Es fehlt jetzt noch die Doku der Installation des RSTPtoWeb-Servers, mit dem man den Stream der Kamera direkt im Browser ansehen kann.

LG

pah

Gisbert

Hallo pah,

vielen Dank für die Entwicklung der Programme und deiner Beschreibung im Wiki. Die Kamera ist ja bei Stiftung Warentest sehr gut, bzw. weit vorne bewertet - und dabei ja auch recht preiswert.

Ich hab noch Fragen zu Kamera-Events und deren Speicherung auf dem Fhem-Server. Dazu ist ist eine Speicherkarte (Empfehlung 16 GB) nötig. Können auch einzelne Bilder gespeichert werden und anschließend auf den Fhem-Server übertragen werden? Ist es praktikabel, direkt nach einem Event Bilder oder Videos zu übertragen und lässt sich das mit deinem Programm automatisiert durchführen - ich vermute ja, kann es aber ohne Kamera nicht beurteilen. Falls das geht, in welchem Zeitraum vermutest du die Speicherung, eher schnell im Sekundenbereich oder kann es dauern? Könnte die Kamera mit der Eventerkennung an der Haustür genutzt werden, um eine Meldung aufs Handy zu bekommen und zu sehen, wer da ist, möglichst bevor der- oder diejenige bereits den Rückzug angetreten hat? Das erfordert Push-Dienste, die bei mir schon vorhanden wären.

Viele Grüße Gisbert
Proxmox | UniFi | Homematic, VCCU, HMUART | ESP8266 | ATtiny85 | Wasser-, Stromzähler | tuya local | Wlan-Kamera | SIGNALduino, Rauchmelder FA21/22RF | RHASSPY | DEYE | JK-BMS | ESPHome | Panasonic Heishamon

Prof. Dr. Peter Henning

#24
Nein, so funktioniert das nicht. Die Kamera sendet zwar in Echtzeit Events an die App. Ich habe aber fast einen Tag lang versucht, einen Mechanismus zu finden, das abzugreifen - ohne Erfolg. Die Events, die man abgreifen kann, liegen also alle in der Vergangenheit, mit fertigen Clips auf der Karte. Auch einen Push-Mechanismus für Alarme gibt es nach gegenwärtigen Stand nicht.

Einzelne Bilder holt man aus dem Stream, ganz ohne SD-Karte, die liegen also schon per App auf dem Smartphone oder auf dem Fhem-Server, nicht auf der Kamera. Das kann man natürlich durch eine Türklingel triggern und dann das Bild verwenden. So mache ich das seit etlichen Jahren auch im Doorpi-Projekt.

LG

pah