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