############################################## # $Id$ # # Protokoll: # Prefix (5a a5), Anzahl Nutzbytes (2 Byte), Payload, Checksumme (FF - LowByte der Summe aller Payloadbytes), Postfix (5b b5) # Antwort von Dose hat immer die letzen 3 Bloecke der MAC vom 11-13 Byte # # Payload immer in "|" # Init1 (vom Server): # 5a a5 00 07|02 05 0d 07 05 07 12|c6 5b b5 # ** ** ** ** ** ** ** scheinen zufaellig zu sein # 5a a5 00 01|02|fd 5b b5 # Antwort auf Init1 von Dose: # 5A A5 00 0B|03 01 0A C0 32 23 62 8A 7E 01 C2|AF 5B B5 # MM MM MM ** MM: letzte 3 Stellen der MAC, ** scheinbar eine Checksumme basierend auf den 6 zufaelligen Bytes von Init1 # Init2 (vom Server): # 5a a5 00 02|05 01|f9 5b b5 # Antwort auf Init2 von Dose: # 5A A5 00 12|07 01 0A C0 32 23 62 8A 7E 00 01 06 AC CF 23 62 8A 7E|5F 5B B5 # MM MM MM MM: letzte 3 Stellen der MAC # MM MM MM MM MM MM MM: komplette MAC # 5A A5 00 12|07 01 0A C0 32 23 62 8A 7E 00 02 05 00 01 01 08 11|4C 5B B5 Anzahl Bytes stimmt nicht! ist aber immer so # 5A A5 00 15|90 01 0A E0 32 23 62 8A 7E 00 00 00 81 11 00 00 01 00 00 00 00|32 5B B5 Status der Dose (wird auch immer bei Zustandsaenderung geschickt) # MM MM MM MM: letzte 3 Stellen der MAC # qq qq: Schaltquelle 81=lokal geschaltet, 11=remote geschaltet # oo oo: Schaltzustand ff=an, 00=aus # Danach kommt alle x Sekunden ein Heartbeat von der Dose: # 5A A5 00 09|04 01 0A C0 32 23 62 8A 7E|71 5B B5 # MM MM MM # Antwort vom Server (wenn die nicht kommt blinkt Dose wieder und muss neu initialisiert werden): # 5a a5 00 01|06|f9 5b b5 #--------------------------------------------------------------------------------------------------------- # Einschalten der Dose: # 5a a5 00 17|10 01 01 0a e0 32 23 62 8a 7e ff fe 00 00 10 11 00 00 01 00 00 00 ff|26 5b b5 # MM MM MM # Ausschalten der Dose # 5a a5 00 17|10 01 01 0a e0 32 23 62 8a 7e ff fe 00 00 10 11 00 00 01 00 00 00 00|25 5b b5 # MM MM MM # beides wird quittiert (ebenso wird auch bei lokaler betaetigung quittiert) -> siehe 3. Antwort auf Init 2 package main; use strict; use warnings; use SetExtensions; use TcpServerUtils; use constant { PREFIX => pack('C*', (0x5a,0xa5)), POSTFIX => pack('C*', (0x5b,0xb5)), INIT1A => pack('C*', (0x02,0x05,0x0d,0x07,0x05,0x07,0x12)), }; my $prefix = pack('C*', (0x5a,0xa5)); my $postfix = pack('C*', (0x5b,0xb5)); my $init1a = pack('C*', (0x02,0x05,0x0d,0x07,0x05,0x07,0x12)); my $init1b = pack('C*', (0x02)); my $init2 = pack('C*', (0x05,0x01)); my $hbeat = pack('C*', (0x06)); my $switch1 = pack('C*', (0x10,0x01,0x01,0x0a,0xe0,0x32,0x23)); my $switch2 = pack('C*', (0xff,0xfe,0x00,0x00,0x10,0x11,0x00,0x00,0x01,0x00,0x00,0x00)); my $dosehb = pack('C*', (0x00,0x09,0x04,0x01,0x0A,0xC0,0x32,0x23)); my $cinit1 = pack('C*', (0x03,0x01,0x0a,0xc0,0x32,0x23)); my $cmac = pack('C*', (0x07,0x01,0x0a,0xc0,0x32,0x23)); my $cswitch = pack('C*', (0x90,0x01,0x0a,0xe0,0x32,0x23)); my $timeout = 60; ##################################### sub GHoma_Initialize($) { # my ($hash) = @_; $hash->{SetFn} = "GHoma_Set"; # evtl. noch in define rein!!! $hash->{DefFn} = "GHoma_Define"; $hash->{ReadFn} = "GHoma_Read"; # wird von der globalen loop aufgerufen (ueber $hash->{FD} gefunden), wenn Daten verfuegbar sind $hash->{UndefFn} = "GHoma_Undef"; $hash->{AttrFn} = "GHoma_Attr"; $hash->{StateFn} = "GHoma_State"; $hash->{AttrList} = "restoreOnStartup:last,on,off restoreOnReinit:last,on,off blocklocal:yes,no ". "allowfrom connectTimeout connectInterval"; $hash->{noAutocreatedFilelog} = 1; # kein Filelog bei Autocreate anlegen $hash->{ShutdownFn} = "GHoma_Shutdown"; } ##################################### sub GHoma_ClientConnect($) { # im Mom unnuetz my ($hash) = @_; my $name = $hash->{NAME}; $hash->{DEF} =~ m/^(IPV6:)?(.*):(\d+)$/; my ($isIPv6, $server, $port) = ($1, $2, $3); Log3 $name, 4, "$name: Connecting to $server:$port..."; my @opts = ( PeerAddr => "$server:$port", Timeout => AttrVal($name, "connectTimeout", 60), ); my $client; if($hash->{SSL}) { $client = IO::Socket::SSL->new(@opts); } else { $client = IO::Socket::INET->new(@opts); } if($client) { $hash->{FD} = $client->fileno(); $hash->{CD} = $client; # sysread / close won't work on fileno $hash->{BUF} = ""; $hash->{CONNECTS}++; $selectlist{$name} = $hash; $hash->{STATE} = "Connected"; RemoveInternalTimer($hash); Log3 $name, 3, "$name: connected to $server:$port"; syswrite($hash->{CD}, ( GHoma_BuildString($init1a) . GHoma_BuildString($init1b) ) ); InternalTimer(gettimeofday()+ $timeout + 30, "GHoma_Timer", $hash,0); } else { GHoma_ClientDisconnect($hash, 1); } } ##################################### sub GHoma_ClientDisconnect($$) { # im Mom unnuetz my ($hash, $connect) = @_; my $name = $hash->{NAME}; close($hash->{CD}) if($hash->{CD}); delete($hash->{FD}); delete($hash->{CD}); delete($selectlist{$name}); $hash->{STATE} = "Offline"; InternalTimer(gettimeofday()+AttrVal($name, "connectInterval", 60), "GHoma_ClientConnect", $hash, 0); if($connect) { Log3 $name, 4, "$name: Connect failed."; } else { Log3 $name, 3, "$name: Offline"; } } ##################################### sub GHoma_Shutdown($) { # my ($hash) = @_; return unless defined $hash->{Id}; #nicht für Server # state auf letzten Schaltwert setzen oder auf fixen Startwert (wird bereitsbeim Shutdown ausgefuehrt) if (AttrVal($hash->{NAME},"restoreOnStartup","last") eq "on") { readingsSingleUpdate($hash, "state", "on", 1); } elsif (AttrVal($hash->{NAME},"restoreOnStartup","last") eq "last" && defined $hash->{LASTSTATE} && $hash->{LASTSTATE} eq "on" ) { readingsSingleUpdate($hash, "state", "on", 1); } else { readingsSingleUpdate($hash, "state", "off", 1); } return undef; } ##################################### sub GHoma_Define($$$) { # my ($hash, $def) = @_; #my @a = split("[ \t][ \t]*", $def); my ($name, $type, $pport, $global) = split("[ \t]+", $def); my $port = $pport; $port =~ s/^IPV6://; my $isServer = 1 if(defined($port) && $port =~ m/^\d+$/); my $isClient = 1 if($port && $port =~ m/^(.+):\d+$/); my $isSerCli = 1 if(defined($port) && $port =~ m/^([\da-f]{6})$/i); #return "Usage: define GHoma { [IPV6:]| }" if(!($isServer || $isClient || $isSerCli)); return "Usage: define GHoma { [IPV6:] }" if(!($isServer || $isClient || $isSerCli)); #$hash->{DeviceName} = $pport; if($isSerCli) { #ServerClient #my $name = $a[0]; # my $addr = $a[2]; #$hash->{Id} = pack('C*', ( hex(substr($pport,0,2)), hex(substr($pport,2,2)), hex(substr($pport,4,2)) ) ); $hash->{Id} = $pport; return; } # Make sure that fhem only runs once if($isServer) { my $ret = TcpServer_Open($hash, $pport, "global"); if($ret && !$init_done) { Log3 $name, 1, "$ret. Exiting."; exit(1); } return $ret; } if($isClient) { $hash->{isClient} = 1; GHoma_ClientConnect($hash); } return; } ##################################### sub GHoma_BuildString($) { # Botschaft zum senden erzeugen my ($data) = @_; my $count = pack('n*', length($data)); my $checksum = pack ('C*', 0xFF - (unpack("%8c*", $data)) ); #(my $smsg = ($prefix . $count . $data . $checksum . $postfix)) =~ s/(.|\n)/sprintf("%.2X ",ord($1))/eg; #Log3 undef, 1, "GHoma TX: $smsg"; return $prefix . $count . $data . $checksum . $postfix; } # ##################################### sub GHoma_moveclient($$) { # Handles von temporaerem Client zu Statischem uebertragen und Temporaeren dann loeschen my ($thash, $chash) = @_; if(defined($chash->{CD})) { # alte Verbindung entfernen, falls noch offen close($chash->{CD}); delete($chash->{CD}); #delete($selectlist{$chash->{NAME}}); delete($chash->{FD}); # Avoid Read->Close->Write } $chash->{FD} = $thash->{FD}; $chash->{CD} = $thash->{CD}; $chash->{SNAME} = $thash->{SNAME}; my @client = split(":",$thash->{NAME}); $chash->{IP} = $client[1]; $chash->{PORT} = $client[2]; $selectlist{$chash->{NAME}} = $chash; readingsSingleUpdate($chash, "state", "Initialize...", 1); delete($selectlist{$thash->{NAME}}); delete $thash->{FD}; CommandDelete(undef, $thash->{NAME}); syswrite( $chash->{CD}, GHoma_BuildString($init2) ); InternalTimer(gettimeofday()+ $timeout, "GHoma_Timer", $chash,0); } ##################################### sub GHoma_Read($) { # wird von der globalen loop aufgerufen (ueber $hash->{FD} gefunden), wenn Daten verfuegbar sind my ($hash) = @_; my $name = $hash->{NAME}; if($hash->{SERVERSOCKET}) { # Accept and create a child my $chash = TcpServer_Accept($hash, "GHoma"); return if(!$chash); Log3 $name, 4, "$name: angelegt: $chash->{NAME}"; syswrite($chash->{CD}, ( GHoma_BuildString($init1a) . GHoma_BuildString($init1b) ) ); InternalTimer(gettimeofday()+ $timeout, "GHoma_Timer", $chash,0); $chash->{CD}->flush(); return; } my $buf; my $ret = sysread($hash->{CD}, $buf, 256); if(!defined($ret)) { if($hash->{isClient}) { Log3 $name, 1, "$name \$buf nicht definiert"; GHoma_ClientDisconnect($hash, 0); } else { CommandDelete(undef, $name); } return; } if ( substr($buf,0,10) eq ($prefix . $dosehb )) { # Heartbeat (Dosen Id wird nicht ueberprueft) #DevIo_SimpleWrite($hash, GHoma_BuildString($hbeat) , undef); RemoveInternalTimer($hash); $buf =~ s/(.|\n)/sprintf("%.2X ",ord($1))/eg; #empfangene Zeichen in Hexwerte wandeln Log3 $name, 5, "$name empfangen: $buf"; syswrite( $hash->{CD}, GHoma_BuildString($hbeat) ); Log3 $hash, 5, "$hash->{NAME} Heartbeat gesendet"; InternalTimer(gettimeofday()+ $timeout, "GHoma_Timer", $hash,0); } else { # alles ausser Heartbeat my @msg = split(/$prefix/,$buf); foreach (@msg) { next if ( $_ eq "" ); if ( hex(unpack('H*', substr($_,length($_)-2,2))) != hex(unpack('H*', $postfix ))) { # Check Postfix Log3 $hash, 1, "$hash->{NAME} Fehler: postfix = " . unpack('H*', substr($_,length($_)-2,2)); next; } if ( hex(unpack('H*', substr($_,length($_)-3,1))) != ( 0xFF - unpack("%8c*", substr($_,2,length($_)-5) ) ) ) { # Check Checksum Log3 $hash, 1, "$hash->{NAME} Fehler: Checksum soll = " . hex(unpack('H*', substr($_,length($_)-3,1))) . " ist = ". ( 0xFF - unpack("%8c*", substr($_,2,length($_)-5) ) ); next; } if ( hex(unpack('H*', substr($_,0,2))) != ( length($_) - 5 ) ) { # Check Laenge Log3 $hash, 4, "$hash->{NAME} laengesoll = " . hex(unpack('H*', substr($_,0,2))) . " laengeist = " . ( length($_) - 5 ) } (my $smsg = $_) =~ s/(.|\n)/sprintf("%.2X ",ord($1))/eg; # empfangene Zeichen in Hexwerte wandeln Log3 $hash, 5, "$hash->{NAME} RX: 5A A5 $smsg"; # ...und ins Log schreiben if ( substr($_,2,6) eq ($cinit1)) { # Antwort auf erstes Init #$hash->{Id} = substr($_,8,3); $hash->{Id} = unpack('H*', substr($_,8,3) ); unless ($hash->{isClient}) { # fuer Server Loesung bei erster Antwort von Dose nach bestehendem Device mit gleicher Id suchen und Verbindung auf dieses Modul uebertragen my $clientdefined = undef; foreach my $dev (devspec2array("TYPE=$hash->{TYPE}")) { # bereits bestehendes define mit dieser Id suchen if ($hash->{Id} eq InternalVal($dev,"Id","") && $hash->{NAME} ne $dev && InternalVal($dev,"TEMPORARY","") ne "1") { #Log3 $hash, 5, "$hash->{NAME}: $dev passt -> Handles uebertragen"; GHoma_moveclient($hash, $defs{$dev}); $clientdefined = 1; last } } unless ( defined $clientdefined) { # ...ein Neues anlegen, falls keins existiert #my $id = unpack('H*', $hash->{Id} ); #Log3 $name, 4, "GHoma Unknown device $id, please define it"; #DoTrigger("global", "UNDEFINED GHoma_$id GHoma $id"); #GHoma_moveclient($hash, $defs{"GHoma_$id"}) if ($defs{"GHoma_$id"}); Log3 $name, 4, "GHoma Unknown device $hash->{Id}, please define it"; DoTrigger("global", "UNDEFINED GHoma_$hash->{Id} GHoma $hash->{Id}"); GHoma_moveclient($hash, $defs{"GHoma_$hash->{Id}"}) if ($defs{"GHoma_$hash->{Id}"}); } } else { readingsSingleUpdate($hash, "state", "Initialize...", 1); syswrite( $hash->{CD}, GHoma_BuildString($init2) ); RemoveInternalTimer($hash); InternalTimer(gettimeofday()+ $timeout, "GHoma_Timer", $hash,0); } } elsif ( substr($_,2,6) eq $cmac && substr($_,8,3) eq substr($_,17,3) ) { # Nachricht mit MAC (kommt unter Anderem als Antwort auf Init2) my $mac; for my $i (0...5) { # MAC formattieren $mac .= sprintf("%.2X",ord( substr($_,14+$i,1) )); last if $i == 5; $mac .= ":"; } $hash->{MAC} = $mac; } elsif ( substr($_,2,6) eq $cswitch && (( length($_) - 5 ) == 0x15 ) ) { # An oder Aus my $id = unpack('H*', substr($_,8,3) ); my $rstate = hex(unpack('H*', substr($_,22,1))) == 0xFF ? "on" : "off"; my $src = hex(unpack('H*', substr($_,14,1))) == 0x81 ? "local" : "remote"; if ( defined $hash->{LASTSTATE} && $hash->{STATE} eq "Initialize..." ) { # wenn dies erste Statusbotschaft nach Anmeldung my $nstate = AttrVal($name, "restoreOnReinit", "last"); if ( $nstate ne "last" && $nstate ne $rstate ) { GHoma_Set( $hash, $hash->{NAME}, $nstate ); } elsif ($nstate eq "last" && $hash->{LASTSTATE} ne $rstate) { GHoma_Set( $hash, $hash->{NAME}, $hash->{LASTSTATE} ); } } elsif ($src eq "local") { # bei schalten direkt an Steckdose soll... if (AttrVal($name, "blocklocal", "no") eq "yes") { # ...wieder zurueckgeschaltet werden, wenn Attribut blocklocal yes ist GHoma_Set($hash, $hash->{NAME}, $hash->{LASTSTATE}); } else { # ...laststate angepasst werden (um bei reinit richtigen wert zu haben) $hash->{LASTSTATE} = $rstate; } } if (defined $hash->{SNAME} && defined $defs{$hash->{SNAME}} ) { # Readings auch im Server eintragen readingsBeginUpdate($defs{$hash->{SNAME}}); readingsBulkUpdate($defs{$hash->{SNAME}}, $id .'_state', $rstate); readingsBulkUpdate($defs{$hash->{SNAME}}, $id .'_source', $src); readingsEndUpdate($defs{$hash->{SNAME}}, 1); } readingsBeginUpdate($hash); readingsBulkUpdate($hash, 'state', $rstate); readingsBulkUpdate($hash, 'source', $src); readingsEndUpdate($hash, 1); } } } #Log3 $name, 5, "$name empfangen: $buf"; return } ##################################### sub GHoma_Timer($) { # wird ausgeloest wenn heartbeat nicht mehr kommt my ($hash) = @_; Log3 $hash, 3, "$hash->{NAME}: Timer abgelaufen"; readingsSingleUpdate($hash, "state", "offline", 1); GHoma_ClientDisconnect($hash, 0) if $hash->{isClient}; return TcpServer_Close($hash) if defined $hash->{FD}; #DevIo_Disconnected($hash); } ##################################### sub GHoma_Attr(@) { # my @a = @_; my $hash = $defs{$a[1]}; # if($a[0] eq "set" && $a[2] eq "SSL") { # TcpServer_SetSSL($hash); # if($hash->{CD}) { # my $ret = IO::Socket::SSL->start_SSL($hash->{CD}); # Log3 $a[1], 1, "$hash->{NAME} start_SSL: $ret" if($ret); # } # } return undef; } ##################################### sub GHoma_Set($@) { # my ($hash, @a) = @_; return undef unless defined $hash->{Id}; # set fuer den Server ausblenden my $name = $a[0]; my $type = $a[1]; my @sets = ('on:noArg', 'off:noArg'); my $status = ReadingsVal($hash->{NAME},"state",""); if($type eq "on") { $type = pack('C*', (0xff)); readingsSingleUpdate($hash, "state", "set_on", 1) if ( $status =~ m/([set_]?o[n|ff])$/i ); $hash->{LASTSTATE} = "on"; } elsif($type eq "off") { $type = pack('C*', (0x00)); readingsSingleUpdate($hash, "state", "set_off", 1) if ( $status =~ m/([set_]?o[n|ff])$/i ); $hash->{LASTSTATE} = "off"; } else { my $slist = join(' ', @sets); return SetExtensions($hash, $slist, @a); } if (defined $hash->{CD}) { syswrite( $hash->{CD}, GHoma_BuildString($switch1 . pack('C*', ( hex(substr($hash->{Id},0,2)), hex(substr($hash->{Id},2,2)), hex(substr($hash->{Id},4,2)) ) ) . $switch2 . $type) ); } return undef; } ##################################### sub GHoma_State($$$$) { # reload readings at FHEM start my ($hash, $tim, $sname, $sval) = @_; Log3 $hash, 4, "$hash->{NAME}: $sname kann auf $sval wiederhergestellt werden $tim"; if ( $sname eq "state" && defined $hash->{Id} ) { #wenn kein Server $hash->{LASTSTATE} = $sval; readingsSingleUpdate($hash, "state", "offline", 1) } return; } ##################################### sub GHoma_Undef($$) { # my ($hash, $arg) = @_; RemoveInternalTimer($hash); return TcpServer_Close($hash) if defined $hash->{FD}; } ##################################### 1; =pod =item device =item summary controls an G-Homa wlan adapter plug =item summary_DE Steuerung einer G-Homa Wlan Steckdose =begin html

GHoma

(en | de)
      Connects fhem to an G-Homa adapter plug

      ATTENTION!:
      With an actual firmware and after firmware update, http access will be disabled.
      Network parameters cannot changed anymore. The only way to use the plug again with FHEM is to change route DNS requests from G-Homa plug to plug.g-homa.com to your FHEM server.
      preliminary:
    • Configure WLAN settings:
      bring device in AP mode (press button for more than 3s, repeat this step until the LED is permanently on)
      Now connect with your computer to G-Home network.
      Browse to 10.10.100.254 (username:password = admin:admin)
      In STA Setting insert your WLAN settings
    • Configure Network Parameters setting:
      Other Setting -> Protocol to TCP-Client
      Other Setting -> Port ID (remember value for FHEM settings)
      Other Setting -> Server Address (IP of your FHEM Server)
    • Optional:
      Block all outgoing connections for G-Homa in your router.


    Define
      define <name> GHoma <port>
      Specifies the GHoma server device.
      New adapters will be added automaticaly after first connection.
      You can also manyally add an adapter:
      define <name> GHoma <Id>
      where Id is the last 6 numbers of the plug's MAC address
      Example: MAC= AC:CF:23:A5:E2:3B -> Id= A5E23B

    Set
      set <name> <value>

      where value is one of:
        off
        on
      The set extensions are also supported.

    Attributes
      For plug devices:
      • restoreOnStartup
        Restore switch state after reboot
        Default: last, valid values: last, on, off

      • restoreOnReinit
        Restore switch state after reconnect
        Default: last, valid values: last, on, off

      • blocklocal
        Restore switch state to reading state immideately after local switching
        Default: no, valid values: no, yes

      For server devices:
      • allowfrom
        Regexp of allowed ip-addresses or hostnames. If set, only connections from these addresses are allowed.

    • readingFnAttributes

=end html =begin html_DE

GHoma

(en | de)
      Verbindet fhem mit einem G-Homa Zwischenstecker

      Achtung!:
      Mit aktueller Firmware und nach einem Firmware Update ist der integrierte Webserver nicht mehr erreichbar.
      Dadurch lassen sich keine Einstellungen mehr anpassen. Die einzige Möglichkeit ist, die DNS anfragen der G-Homa Dose an plug.g-homa.com zum FHEM server umzuleiten.
      Vorbereitung:
    • WLAN konfigurieren:
      Gerät in den AP modus bringen (Knopf für mehr als 3s drücken, diesen Schritt wiederholen bis die LED permanent leuchtet)
      Nun einen Computer mit der SSID G-Home verbinden.
      Im Browser zu 10.10.100.254 (username:passwort = admin:admin)
      In STA Setting WLAN Einstellungen eintragen
    • Network Parameters settings:
      Other Setting -> Protocol auf TCP-Server
      Other Setting -> Port ID (wird später für FHEM benötigt)
      Other Setting -> Server Address (IP Adresse des FHEM Servers)
    • Optional:
      Im Router alle ausgehenden Verbindungen für G-Homa blockieren.


    Define
      define <name> GHoma <port>
      Legt ein GHoma Server device an.
      Neue Zwischenstecker werden beim ersten verbinden automatisch angelegt.
      Diese können aber auch manuell angelegt werden:
      define <name> GHoma <Id>
      Die Id besteht aus den letzten 6 Stellen der MAC Adresse des Zwischensteckers.
      Beispiel: MAC= AC:CF:23:A5:E2:3B -> Id= A5E23B

    Set
      set <name> <value>

      Gültige Werte für value:
        off
        on
      Die set extensions werden auch unterstützt.

    Attributes
      Für Zwischenstecker devices:
      • restoreOnStartup
        Wiederherstellen der Portzustände nach Neustart
        Standard: last, gültige Werte: last, on, off

      • restoreOnReinit
        Wiederherstellen der Portzustände nach Neustart
        Standard: last, gültige Werte: last, on, off

      • blocklocal
        Wert im Reading State sofort nach Änderung über lokale Taste wiederherstellen
        Standard: no, gültige Werte: no, yes

      Für Server devices:
      • allowfrom
        Regexp der erlaubten IP-Adressen oder Hostnamen. Wenn dieses Attribut gesetzt wurde, werden ausschließlich Verbindungen von diesen Adressen akzeptiert.

    • readingFnAttributes

=end html_DE =cut