Katzenklo mit Internetanschluss

Begonnen von Prof. Dr. Peter Henning, 23 Januar 2026, 15:35:12

Vorheriges Thema - Nächstes Thema

Prof. Dr. Peter Henning

Nein, das ist keine Frage, und keine Umfrage - und nur im Anfängerbereich, weil hier fast jeder mitliest.

Wir haben jetzt auch die Katze automatisiert, mit einem automatischen Katzenklo https://www.amazon.de/dp/B0FDGGK194

Die Dinger haben seit der Pandemiezeit insbesondere in chinesischen Hochhaussiedlungen extrem an Beliebtheit gewonnen, und sind inzwischen auch in Europa auf dem Vormarsch. Leider, wie oft bei chinesischer Massenware, mit einer Schnittstelle zur Tuya-Cloud.

Das könnte man zwar auf Tasmota umflashen, allerdings habe ich dort kein entsprechendes Device gefunden.

Anscheinend gibt es aber für Home Assistant eine Lösung, die ohne dieses Umflashen auskommt. Hat jemand damit Erfahrung?

LG

pah




Gisbert

Hallo pah,

es gibt in Fhem eine lokale Tuya-Anwendung: https://forum.fhem.de/index.php?topic=127441.0
Ich hab einen Luftentfeuchter und 2 schaltbare Steckdosen mit Leistungsmessung damit laufen. Beabsichtigt hatte ich die Integration des Luftbefeuchters in Fhem nicht, da ich beim Kauf davon noch nichts wusste.
Es funktioniert alles. Zwischenzeitlich hatte ich noch 2 schaltbare Steckdosen gekauft. Ob ich die Integration von Tuya in Fhem empfehlen kann - eher nicht. Es ist doch arg kompliziert. Jedenfalls plane ich keine weiteren Geräte mit Tuya anzuschaffen.

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

Danke für den Hinweis, ich hatte schon damit geliebäugelt, dieses pytuya selbst anzubinden.
Zitat von: Gisbert am 23 Januar 2026, 20:27:17Jedenfalls plane ich keine weiteren Geräte mit Tuya anzuschaffen.
Na ja, wir leben im 21. Jahrhundert. Da gibt es manche Sachen nur aus China mit der Tuya-Cloud...

Und wenn ich das mit der werbeverseuchten Mist-Cloud von BSH (Bosch-Siemens-Hausgeräte) vergleiche, oder mit der neuen ebenfalls total werbeverseuchten integrierten Verkaufs- und Ladeapp von Shell: Da ist Tuya eindeutig besser, die betreiben sogar eigene Data-Center in unterschiedlichen Weltregionen.

Nachdem ich mal in China gearbeitet habe, gibt es da nur so ein kleines rotes Warnlicht in meiner Wahrnehmung, wenn ich Software aus chinesischer Quelle verwende...

LG

pah

betateilchen

Elektronik rausschmeissen und die Steuerung mit ESP nachbauen?

Nur so eine Idee, ich habe die Anforderung nicht.
-----------------------
Formuliere die Aufgabe möglichst einfach und
setze die Lösung richtig um - dann wird es auch funktionieren.
-----------------------
Lesen gefährdet die Unwissenheit!

Prof. Dr. Peter Henning

Zitat von: betateilchen am 24 Januar 2026, 08:57:17Elektronik rausschmeissen und die Steuerung mit ESP nachbauen?
Gewichtssensoren, Zeitsteuerung der Bewegung => sehr hoher Aufwand...

Ich mache den Thread jetzt hier dicht, habe das Ganze mit diesem tuya-fhempy so halb zum Laufen bekommen.

LG

pah

Prof. Dr. Peter Henning

Doch noch einmal geöffnet, denn jetzt habe ich eine gut funktionierende Lösung.

Vorab: Der Zugang mit fhempy hat immer nur halb geklappt. Offenbar wird dieses Projekt nicht mehr richtig gepflegt, und die ganze Idee der schlecht dokumentierten "Plugins" halte ich für höchst fragwürdig.

Also habe ich mit Hilfe eines anderen Github-Projektes https://github.com/jasonacox/tinytuya
ein lokales Python-Kontrollskript geschaffen, das die Katzentoilette nicht nur auslesen, sondern wunderbar steuern kann.

Das Konzept ist dasselbe, wie bei den Tapo Sicherheitskameras: FHEM ruft Python-Skripte auf, diese melden asynchron an FHEM zurück. Das ist auch für diejenigen, die keine Katzentoilette haben, ein gangbarer Weg, neue Tuya-Geräte zu integrieren. Die werden dann eben cloudfrei betrieben.

Gesteuert wird das über ein Dummy-device
defmod TuyaToilet dummy
attr TuyaToilet setList update_status:noArg clean:noArg empty:noArg child_lock:on,off smart_clean:on,off auto_deodorizer:on,off odourless:on,off induction_delay induction_interval
ein DOIF
defmod TuyaToilet.N DOIF ([TuyaToilet:state] ne "ready") ()\
({TuyaToiletHandler("TuyaToilet","$EVENT")})\
(setreading TuyaToilet state ready)\
\
DOELSEIF\
([+00:05:00])\
({TuyaToiletHandler("TuyaToilet","update_status")})
und ein paar Perl-Funktionen
###############################################################################
#
#  TuyaToiletHandler
#
###############################################################################

sub TuyaToiletHandler($$) {
  my ($name, $event) = @_;

  my $hash = $defs{$name};
  return if(!$hash);

  if(!defined($event) || $event eq '') {
    readingsSingleUpdate($hash, 'error', 'missing event', 1);
    return;
  }

  $event =~ s/^\s+|\s+$//g;

  # Schutz: komplette Reading-/Eventlisten ignorieren
  # typische FHEM-Events enthalten ":" oder ","
  if($event =~ /,|:\s/) {
    Log 3, "TuyaToiletHandler($name): ignored non-command event [$event]";
    return;
  }

  my $python = '/opt/fhem/tuya/.venv/bin/python3';
  my $script = '/opt/fhem/tuya/tuya_control.py';
  my $cmd;

  my ($action, $value) = split(/\s+/, $event, 2);
  $action = '' if !defined($action);
  $value  = '' if !defined($value);

  if ($action eq 'update_status') {
    $cmd = qq($python $script status >/dev/null 2>&1 &);

  } elsif ($action eq 'clean') {
    $cmd = qq($python $script clean >/dev/null 2>&1 &);

  } elsif ($action eq 'empty') {
    $cmd = qq($python $script empty >/dev/null 2>&1 &);

  } elsif ($action eq 'child_lock') {
    $value = 'off' if $value eq '';
    $cmd = qq($python $script child_lock $value >/dev/null 2>&1 &);

  } elsif ($action eq 'smart_clean') {
    $value = 'off' if $value eq '';
    $cmd = qq($python $script smart_clean $value >/dev/null 2>&1 &);

  } elsif ($action eq 'auto_deodorizer') {
    $value = 'off' if $value eq '';
    $cmd = qq($python $script auto_deodorizer $value >/dev/null 2>&1 &);

  } elsif ($action eq 'odourless') {
    $value = 'off' if $value eq '';
    $cmd = qq($python $script odourless $value >/dev/null 2>&1 &);

  } elsif ($action eq 'induction_delay') {
    if($value eq '') {
      readingsSingleUpdate($hash, 'error', 'missing value for induction_delay', 1);
      return;
    }
    $cmd = qq($python $script induction_delay $value >/dev/null 2>&1 &);

  } elsif ($action eq 'induction_interval') {
    if($value eq '') {
      readingsSingleUpdate($hash, 'error', 'missing value for induction_interval', 1);
      return;
    }
    $cmd = qq($python $script induction_interval $value >/dev/null 2>&1 &);

  } else {
    readingsSingleUpdate($hash, 'error', "unknown event: $event", 1);
    return;
  }

  system($cmd);
  return;
}

###############################################################################
#
#  TuyaReturnHandler
#
###############################################################################

sub TuyaReturnHandler($$$) {
  my ($name, $command, $json) = @_;

  my $hash = $defs{$name};
  return if(!$hash);

  if(!defined($json) || $json eq '') {
    readingsSingleUpdate($hash, 'error', 'empty json', 1);
    return;
  }

  my $data;
  eval {
    $data = decode_json($json);
  };
  if($@ || ref($data) ne 'HASH') {
    readingsSingleUpdate($hash, 'error', "invalid json for command $command", 1);
    return;
  }

  # --- Fehler-Rückgabe aus Python
  if(defined($data->{result}) && $data->{result} eq 'error') {
    my $msg = $data->{message} // 'unknown error';
    readingsBeginUpdate($hash);
    readingsBulkUpdate($hash, 'error', $msg);
    readingsBulkUpdate($hash, 'last_command', $command);
    readingsBulkUpdate($hash, 'last_command_result', 'error');
    readingsBulkUpdate($hash, 'state', 'error');
    readingsEndUpdate($hash, 1);
    return;
  }

  my %map = (
    '6'   => 'cat_weight',
    '7'   => 'excretion_times_day',
    '8'   => 'excretion_time_day',
    '17'  => 'deodorization',
    '22'  => 'fault',
    '101' => 'clean',
    '102' => 'empty',
    '103' => 'trash_status',
    '104' => 'monitoring',
    '105' => 'induction_clean',
    '106' => 'clean_time',
    '107' => 'clean_time_switch',
    '108' => 'clean_taste',
    '109' => 'clean_taste_switch',
    '110' => 'not_disturb',
    '111' => 'clean_notice',
    '112' => 'toilet_notice',
    '113' => 'net_notice',
    '114' => 'child_lock',
    '115' => 'calibration',
    '116' => 'unit',
    '117' => 'induction_delay',
    '118' => 'induction_interval',
    '119' => 'store_full_notify',
    '120' => 'odourless',
    '121' => 'smart_clean',
    '122' => 'not_disturb_switch',
    '123' => 'usage_times',
    '124' => 'capacity_calibration',
    '125' => 'sand_surface_calibration',
    '126' => 'auto_deodorizer',
    '127' => 'detection_sensitivity',
    '128' => 'number',
    '129' => 'lbs',
    '130' => 'deport_mode',
  );

  # --------------------------------------------------
  # status
  # --------------------------------------------------
  if($command eq 'status') {
    my $dps = $data->{dps};

    if(ref($dps) ne 'HASH') {
      readingsSingleUpdate($hash, 'error', 'status result without dps', 1);
      return;
    }

    readingsBeginUpdate($hash);
    readingsBulkUpdate($hash, 'error', 'none');
    readingsBulkUpdate($hash, 'last_command', 'status');
    readingsBulkUpdate($hash, 'last_command_result', 'ok');

    # --- rohe DPS als normale Readings schreiben
    foreach my $dp (sort { $a <=> $b } keys %{$dps}) {
      my $reading = exists $map{$dp} ? $map{$dp} : sprintf("dp_%03d", $dp);
      my $orig    = $dps->{$dp};
      my $value   = $orig;

      if(ref($orig) =~ /Boolean/) {
        $value = $orig ? 'on' : 'off';
      } elsif(ref($orig)) {
        eval {
          $value = encode_json($orig);
        };
        $value = "$value";
      }

      readingsBulkUpdate($hash, $reading, $value);
    }

    # --- virtuelle Readings ableiten
    my $cat_weight      = exists $dps->{'6'}   ? $dps->{'6'}   : 0;
    my $excretion_count = exists $dps->{'7'}   ? $dps->{'7'}   : 0;
    my $deodorization   = exists $dps->{'17'}  ? $dps->{'17'}  : 0;
    my $fault           = exists $dps->{'22'}  ? $dps->{'22'}  : 0;
    my $clean           = exists $dps->{'101'} ? $dps->{'101'} : 0;
    my $empty           = exists $dps->{'102'} ? $dps->{'102'} : 0;
    my $trash_status    = exists $dps->{'103'} ? $dps->{'103'} : '';
    my $child_lock      = exists $dps->{'114'} ? $dps->{'114'} : 0;

    my $weight_num = 0;
    $weight_num = $cat_weight if(defined($cat_weight) && $cat_weight =~ /^\d+$/);

    my $excretion_num = 0;
    $excretion_num = $excretion_count if(defined($excretion_count) && $excretion_count =~ /^\d+$/);

    my $fault_num = 0;
    $fault_num = $fault if(defined($fault) && $fault =~ /^\d+$/);

    my $occupied = 0;
    my $cat_size = 'none';

    # Gewichtslogik:
    # < 2000 g => kein Tier / typischer Sackwechselwert etc.
    # 2000..3999 g => leichte Katze
    # >= 4000 g => schwere Katze
    if($weight_num >= 4000) {
      $occupied = 1;
      $cat_size = 'large';
    } elsif($weight_num >= 2000) {
      $occupied = 1;
      $cat_size = 'small';
    }

    my $fault_active       = $fault_num != 0 ? 1 : 0;
    my $cleaning_active    = $clean ? 1 : 0;
    my $emptying_active    = $empty ? 1 : 0;
    my $deodorizing_active = $deodorization ? 1 : 0;
    my $child_locked       = $child_lock ? 1 : 0;
    my $recent_use         = $excretion_num > 0 ? 1 : 0;

    my $weight_kg = '';
    if($weight_num > 0) {
      $weight_kg = sprintf("%.3f", $weight_num / 1000);
    }

    readingsBulkUpdate($hash, 'virtual_occupied',           $occupied ? 'yes' : 'no');
    readingsBulkUpdate($hash, 'virtual_cat_size',           $cat_size);
    readingsBulkUpdate($hash, 'virtual_fault',              $fault_active ? 'yes' : 'no');
    readingsBulkUpdate($hash, 'virtual_fault_code',         $fault_num);
    readingsBulkUpdate($hash, 'virtual_cleaning_active',    $cleaning_active ? 'yes' : 'no');
    readingsBulkUpdate($hash, 'virtual_emptying_active',    $emptying_active ? 'yes' : 'no');
    readingsBulkUpdate($hash, 'virtual_deodorizing_active', $deodorizing_active ? 'yes' : 'no');
    readingsBulkUpdate($hash, 'virtual_child_locked',       $child_locked ? 'yes' : 'no');
    readingsBulkUpdate($hash, 'virtual_recent_use',         $recent_use ? 'yes' : 'no');
    readingsBulkUpdate($hash, 'virtual_usage_today',        $excretion_num);
    readingsBulkUpdate($hash, 'virtual_weight_kg',          $weight_kg);

    # optional hilfreiche Zeitstempel
    if($occupied) {
      readingsBulkUpdate($hash, 'virtual_last_occupied', TimeNow());
    }
    if($recent_use) {
      readingsBulkUpdate($hash, 'virtual_last_usage_day_seen', TimeNow());
    }

    # trash_status vorsichtig behandeln, solange Semantik nicht sicher bekannt ist
    my $bin_full = 'unknown';
    if(defined($trash_status) && $trash_status ne '') {
      if($trash_status =~ /full|voll/i) {
        $bin_full = 'yes';
      } elsif($trash_status =~ /^(0|false|off)$/i) {
        $bin_full = 'no';
      }
    }
    readingsBulkUpdate($hash, 'virtual_bin_full', $bin_full);

    # --- virtuelle Betriebsart + state
    my $virtual_mode = 'ready';
    my $state        = 'ready';

    if($fault_active) {
      $virtual_mode = 'fault';
      $state        = "fault_$fault_num";
    } elsif($occupied) {
      $virtual_mode = 'occupied';
      if($cat_size eq 'large') {
        $state = "occupied_large_${weight_num}g";
      } elsif($cat_size eq 'small') {
        $state = "occupied_small_${weight_num}g";
      } else {
        $state = "occupied_${weight_num}g";
      }
    } elsif($emptying_active) {
      $virtual_mode = 'emptying';
      $state        = 'emptying';
    } elsif($cleaning_active) {
      $virtual_mode = 'cleaning';
      $state        = 'cleaning';
    } elsif($deodorizing_active) {
      $virtual_mode = 'deodorizing';
      $state        = 'deodorizing';
    } elsif($child_locked) {
      $virtual_mode = 'child_lock';
      $state        = 'child_lock';
    } elsif($weight_num > 0 && $weight_num < 2000) {
      # typischer Sackwechsel-/Technikwert sichtbar machen
      $virtual_mode = 'idle';
      $state        = "idle_weight_${weight_num}g";
    } else {
      $virtual_mode = 'ready';
      $state        = 'ready';
    }

    readingsBulkUpdate($hash, 'virtual_mode', $virtual_mode);
    readingsBulkUpdate($hash, 'state', $state);

    readingsEndUpdate($hash, 1);
    return;
  }

  # --------------------------------------------------
  # erfolgreiche Schreibkommandos
  # --------------------------------------------------
  if(
       $command eq 'clean'
    || $command eq 'empty'
    || $command eq 'child_lock'
    || $command eq 'smart_clean'
    || $command eq 'auto_deodorizer'
    || $command eq 'odourless'
    || $command eq 'induction_delay'
    || $command eq 'induction_interval'
  ) {
    readingsBeginUpdate($hash);
    readingsBulkUpdate($hash, 'error', 'none');
    readingsBulkUpdate($hash, 'last_command', $command);
    readingsBulkUpdate($hash, 'last_command_result', 'ok');
    readingsEndUpdate($hash, 1);

    TuyaToiletHandler($name, 'update_status');
    return;
  }

  readingsSingleUpdate($hash, 'error', "unknown command: $command", 1);
}

Den Python-Kram habe ich mal hier angehängt, bei Gelegenheit gibt es dazu noch ein Wiki.

LG

pah

Gisbert

Hallo pah,

das ist ja Klasse, dass du doch noch einen Weg gefunden hast. Ich habe einen Aktobis-Entfeuchter, den ich technisch und von der Bedienbarkeit sehr gut finde. Ich hab dieses Gerät einem Bekannten empfohlen, der sich aber für die initiale Anmeldung partout nicht in der Tuya-(oder wie sie auch immer heißt)-Cloud anmelden will.

Ist bei deiner Lösung eine initiale Anmeldung in der Tuya...-Cloud noch nötig?

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

#7
Zitat von: Gisbert am 25 März 2026, 18:18:00Ist bei deiner Lösung eine initiale Anmeldung in der Tuya...-Cloud noch nötig?
Leider ja. Der Grund ist, dass man nur auf diese Weise an einen lokalen Key kommt, der die Kommunikation von FHEM mit dem Device verschlüsselt.

Es ist also nicht nur die Anmeldung des Devices in der Tuya App nötig, sondern zusätzlich ein Tuya Developer Account. Die genaue Beschreibung der Vorgehensweise findet man hier: https://github.com/jasonacox/tinytuya/blob/master/README.md

Also leider nichts, was man einem Laien empfehlen könnte.

Allerdings liefert das im Endeffekt tatsächlich etwas, das ein solches Tuya-Device komplett von der Cloud isolieren kann. Dazu muss man nur die Firewall für dieses Device dicht machen und es eventuell aus der Tuya-App löschen. Der letzte Schritt ist aber gefährlich, bei manchen Geräten wird dann auch der Local Key gelöscht. Muss man ausprobieren und eventuell wieder rückgängig machen. Auch der Wechsel in ein anderes WLAN kann eventuell den Local Key zerstören.


Für andere Tuya-Geräte kann ich nur empfehlen, die Vorgehensweise in dem oben genannten README zu befolgen. Wenn man erstmal den Local Key hat, die Datapoints abgerufen und vielleicht noch mit Hilfe des Tuya-Cloud-Accounts zugeordnet hat, ist das ein stabiles System.

LG

pah