
# $Id: 37_harmony.pm 19170 2019-04-13 12:21:29Z justme1968 $

package main;

use strict;
use warnings;

use Data::Dumper;

use JSON;
use MIME::Base64;
use IO::Socket::INET;
use Encode qw(encode_utf8);
#use XML::Simple qw(:strict);

use HttpUtils;

my $harmony_isFritzBox = undef;
sub
harmony_isFritzBox()
{
  $harmony_isFritzBox = int( qx( [ -f /usr/bin/ctlmgr_ctl ] && echo 1 || echo 0 ) )  if( !defined($harmony_isFritzBox) );

  return $harmony_isFritzBox;
}
sub
harmony_decode_json($)
{
  my ($data) = @_;

  return eval { decode_json($data) } if( harmony_isFritzBox() );

  return eval { JSON->new->utf8(0)->decode($data) };
}

sub
harmony_Initialize($)
{
  my ($hash) = @_;

  $hash->{ReadFn}   = "harmony_Read";

  $hash->{DefFn}    = "harmony_Define";
  $hash->{NotifyFn} = "harmony_Notify";
  $hash->{UndefFn}  = "harmony_Undefine";
  $hash->{SetFn}    = "harmony_Set";
  $hash->{GetFn}    = "harmony_Get";
  $hash->{AttrFn}   = "harmony_Attr";
  $hash->{AttrList} = "disable:1 nossl:1 forceWebSocket:1 $readingFnAttributes";

  #$hash->{FW_detailFn}  = "harmony_detailFn";

  $hash->{FW_deviceOverview} = 1;

  $hash->{startDiscovery} = "harmony_startDiscovery";
}

#####################################


sub
harmony_startDiscovery()
{
  if( defined($modules{harmony}{defptr}{'harmony:discovery'}) ) {
    harmony_sendDiscovery();
  }

  return if( $modules{harmony}{defptr}{'harmony:discovery'} );
  Log3 undef, 3, "harmony: starting discovery" ;

  if( my $send_socket = new IO::Socket::INET ( Proto => 'udp', Broadcast => 1, ReuseAddr=>1, ReusePort=>defined(&SO_REUSEPORT)?1:0) ) {
    if( my $socket = IO::Socket::INET->new( Listen=>10, Blocking=>1, ReuseAddr=>1) ) {
      my $chash = {
               TYPE => 'harmony',
                 NR => $devcount++,
               NAME => 'harmony:discovery',
              STATE => 'discovering',
         sendSocket => $send_socket,
          TEMPORARY => 1,
                 CD => $socket,
                 FD => $socket->fileno(),
               PORT => $socket->sockport,
      };
      $chash->{helper}{discovered} = {};

      $attr{$chash->{NAME}}{room} = 'hidden';

      $modules{harmony}{defptr}{'harmony:discovery'} = $chash;

      $defs{$chash->{NAME}}       = $chash;
      $selectlist{$chash->{NAME}} = $chash;

      harmony_sendDiscovery();

    } else {
      Log3 undef, 2, "harmony: failed to start discovery" ;
    }
  } else {
    Log3 undef, 2, "harmony: failed to start discovery" ;
  }

}
sub
harmony_sendDiscovery()
{
  if( my $chash = $modules{harmony}{defptr}{'harmony:discovery'} ) {
    Log3 undef, 3, "harmony: sending discovery" ;

    my $sin = sockaddr_in(5224, inet_aton('255.255.255.255'));
    $chash->{sendSocket}->send( "_logitech-reverse-bonjour._tcp.local.\n$chash->{PORT}", 0, $sin );

  } else {
    Log3 undef, 2, "harmony: can't send or receive discovery" ;

  }

  foreach my $chash ( values %{$modules{'harmony'}{defptr}} ) {
    my $cname = $chash->{NAME};;
    next if( $cname eq 'harmony:discovery' );
    next if( IsDisabled($cname) );
    next if( $chash->{ConnectionState} eq 'Connected' );
    next if( !defined($chash->{ip}) );
    next if( $chash->{remoteId} );

    RemoveInternalTimer($chash);
    InternalTimer(gettimeofday()+10, "harmony_tryFakeOrigin", $chash, 0);
  }
}
sub
harmony_tryFakeOrigin($)
{
  my ($hash) = @_;
  my $name = $hash->{NAME};

  Log3 $name, 3, "$name no discovery response received, trying fake origin" ;
  my $timeout = $hash->{TIMEOUT} ? $hash->{TIMEOUT} : 2;
  if( my $socket = IO::Socket::INET->new(PeerAddr=>"$hash->{ip}:8088", Timeout=>$timeout) ) {
    Log3 $name, 3, "$name: connected";
    $hash->{protocol} = "WEBSOCKET";
    $hash->{ConnectionState} = "Connected";
    readingsSingleUpdate( $hash, "state", $hash->{ConnectionState}, 1 ) if( $hash->{ConnectionState} ne ReadingsVal($name, "state", "" ) );
    $hash->{LAST_CONNECT} = FmtDateTime( gettimeofday() );

    $hash->{FD}    = $socket->fileno();
    $hash->{CD}    = $socket;         # sysread / close won't work on fileno
    $hash->{CONNECTS}++;
    $selectlist{$name} = $hash;

    $hash->{fakeOrigin} = 1;

    my $ret = "POST / HTTP/1.1\r\n";
    $ret .= harmony_hash2header( {                    'Host' => "$hash->{ip}:8088",
                                                    'Origin' => 'http://localhost.nebula.myharmony.com',
                                              'Content-Type' => 'application/json',
                                            'Content-Length' => 35,
                                } );

    $ret .= "\r\n";
    $ret .= '{"cmd":"connect.discoveryinfo?get"}';
    Log3 $name, 5, "$name: $ret";

    syswrite( $hash->{CD}, $ret );

    return;

  } else {
    Log3 $name, 2, "$name failed to connect to websocket port" ;

  }

  harmony_tryXMPP($hash);
}

sub
harmony_tryXMPP($)
{
  my ($hash) = @_;
  my $name = $hash->{NAME};

  Log3 $name, 3, "$name no discovery response received, trying fallback to xmpp" ;

  harmony_connect($hash);
}
sub
harmony_stopDiscovery()
{
  my $chash = $modules{harmony}{defptr}{'harmony:discovery'};
  return if( !$chash );
  Log3 undef, 3, "harmony: stopping discovery" ;

  close( $chash->{sendSocket} );
  close( $chash->{CD} );

  delete $selectlist{$chash->{NAME}};
  delete $defs{$chash->{NAME}};
  delete $modules{$chash->{TYPE}}{defptr}{'harmony:discovery'};
}


sub
harmony_Define($$)
{
  my ($hash, $def) = @_;

  my @a = split("[ \t][ \t]*", $def);
  my ($param_a, $param_h) = parseParams(\@a);
  @a = @{$param_a};

  return "Usage: define <name> harmony [username password] ip"  if(@a < 3 || @a > 5);
  return "Usage: define <name> harmony [username password] ip"  if(@a == 4 && $a[2] ne "DEVICE" );

  delete( $hash->{helper}{username} );
  delete( $hash->{helper}{password} );

  my $name = $a[0];

  if( @a == 3 ) {
    my $ip = $a[2];
    return "$name: harmony device for '$ip' already defined" if( defined($modules{$hash->{TYPE}}{defptr}{$ip}) && $name ne $modules{$hash->{TYPE}}{defptr}{$ip}{NAME} );

    $hash->{ip} = $ip;

    $modules{$hash->{TYPE}}{defptr}{$ip} = $hash;

  } elsif( @a == 4 ) {
    my $id = $a[3];

    return "$name: harmony device for '$id' already defined" if( defined($modules{$hash->{TYPE}}{defptr}{$id}) && $name ne $modules{$hash->{TYPE}}{defptr}{$id}{NAME} );

    $hash->{id} = $id;
    $modules{$hash->{TYPE}}{defptr}{$id} = $hash;

  } elsif( @a == 5 ) {
    my $ip = $a[4];
    return "$name: harmony device for '$ip' already defined" if( defined($modules{$hash->{TYPE}}{defptr}{$ip}) && $name ne $modules{$hash->{TYPE}}{defptr}{$ip}{NAME} );

    my $username = harmony_encrypt($a[2]);
    my $password = harmony_encrypt($a[3]);

    $hash->{DEF} = "$username $password $ip";
    $hash->{DEF} .= " remoteId=$param_h->{remoteId}" if( $param_h->{remoteId} );

    $hash->{helper}{username} = $username;
    $hash->{helper}{password} = $password;
    $hash->{ip} = $ip;

    $modules{$hash->{TYPE}}{defptr}{$ip} = $hash;
  }

  $hash->{NAME} = $name;

  $hash->{remoteId} = $param_h->{remoteId};

  $hash->{STATE} = "Initialized";
  $hash->{ConnectionState} = "Initialized";

  #$attr{$name}{nossl} = 1 if( !$init_done && harmony_isFritzBox() );

  $hash->{NOTIFYDEV} = "global";

  if( $init_done && !defined($hash->{id}) ) {
    if( !$hash->{remoteId} ) {
      harmony_startDiscovery();
    } else {
      harmony_connect($hash);
    }
  }

  return undef;
}

sub
harmony_Notify($$)
{
  my ($hash,$dev) = @_;

  return if($dev->{NAME} ne "global");
  return if(!grep(m/^INITIALIZED|REREADCFG$/, @{$dev->{CHANGED}}));

  if( !$hash->{remoteId} && !defined($hash->{id}) ) {
    harmony_startDiscovery();
  } else {
    harmony_connect($hash);
  }

  return undef;
}

sub
harmony_Undefine($$)
{
  my ($hash, $arg) = @_;

  if( $hash->{NAME} eq 'harmony:discovery' ) {
    harmony_stopDiscovery();
    $defs{$hash->{NAME}} = $hash;
    return;
  }

  if( defined($hash->{id}) ) {
    delete( $modules{$hash->{TYPE}}{defptr}{$hash->{id}} );
    return undef;
  }

  delete( $modules{$hash->{TYPE}}{defptr}{$hash->{ip}} );


  RemoveInternalTimer($hash);

  harmony_disconnect($hash);

  return undef;
}

sub
harmony_detailFn()
{
  my ($FW_wname, $d, $room, $pageHash) = @_; # pageHash is set for summaryFn.
  my $hash = $defs{$d};

  return if( !defined( $hash->{discoveryinfo} ) );

  my $clientId = $hash->{discoveryinfo}->{setupSessionClient};
  $clientId =~ s/^\w*-//;

  my $hubIP = $hash->{discoveryinfo}->{ip};

  my $hubName = $hash->{discoveryinfo}->{friendlyName};
  $hubName =~ s/ /%20/;

  return "<a href=\"http://sl.dhg.myharmony.com/mobile/2/production/?locale=de-DE&clientId=$clientId&hubIP=$hubIP&hubName=$hubName&settings\" target=\"_blank\">myHarmony config</a><br>"
}




sub
harmony_idOfActivity($$;$)
{
  my ($hash, $label, $default) = @_;

  my $quoted_label = $label;
  $quoted_label =~ s/\./ /g;
  $quoted_label = quotemeta($quoted_label);

  foreach my $activity (@{$hash->{config}{activity}}) {
    return $activity->{id} if( $activity->{label} =~ m/^$label$/ );
    return $activity->{id} if( $activity->{label} =~ m/^$quoted_label$/ );
  }

  return $default;
}
sub
harmony_labelOfActivity($$;$)
{
  my ($hash, $id, $default) = @_;

  foreach my $activity (@{$hash->{config}{activity}}) {
    return $activity->{label} if( $activity->{id} == $id );
  }

  return $default;
}
sub
harmony_activityOfId($$)
{
  my ($hash, $id) = @_;

  foreach my $activity (@{$hash->{config}{activity}}) {
    return $activity if( $activity->{id} == $id );
  }

  return undef;
}

sub
harmony_idOfDevice($$;$)
{
  my ($hash, $label, $default) = @_;

  my $quoted_label = $label;
  $quoted_label =~ s/\./ /g;
  $quoted_label = quotemeta($quoted_label);

  foreach my $device (@{$hash->{config}->{device}}) {
    return $device->{id} if( $device->{label} =~ m/^$label$/ );
    return $device->{id} if( $device->{label} =~ m/^$quoted_label$/ );
  }

  return $default;
}
sub
harmony_labelOfDevice($$;$)
{
  my ($hash, $id, $default) = @_;

  return undef if( $id eq '<unknown>' );
  return undef if( !defined($hash->{config}) );

  foreach my $device (@{$hash->{config}->{device}}) {
    return $device->{label} if( $device->{id} == $id );
  }

  return $default;
}
sub
harmony_deviceOfId($$)
{
  my ($hash, $id) = @_;

  return undef if( !defined($hash->{config}) );

  foreach my $device (@{$hash->{config}->{device}}) {
    return $device if( $device->{id} == $id );
  }

  return undef;
}

sub
harmony_actionOfCommand($$)
{
  my ($device, $command) = @_;
  return undef if( ref($device) ne "HASH" );

  $command = lc($command);

  foreach my $group (@{$device->{controlGroup}}) {
    foreach my $function (@{$group->{function}}) {
      #if( lc($function->{name}) eq $command ) {
      if( lc($function->{name}) =~ m/^$command$/ ) {
        return harmony_decode_json($function->{action});
      }
    }
  }

  return undef;
}

sub
harmony_hubOfDevice($)
{
  my ($id) = @_;

  foreach my $d (sort keys %defs) {
    next if( !defined($defs{$d}) );
    next if( $defs{$d}->{TYPE} ne "harmony" );
    next if( $defs{$d}->{id} );
    next if( !harmony_deviceOfId($defs{$d}, $id) );
    Log3 undef, 3, "harmony: IODev for device $id is $d" ;
    return $d;
  }
}

sub
harmony_Set($$@)
{
  my ($hash, $name, $cmd, @params) = @_;
  my ($param_a, $param_h) = parseParams(\@params);
  my ($param, $param2) = @{$param_a};
  #$cmd = lc( $cmd );

  if( $hash->{sendSocket} ) {
    my $list = "discover:noArg";

    if( $cmd eq "discover" ) {
      harmony_sendDiscovery();
      return;
    }

    return "Unknown argument $cmd, choose one of $list";
  }

  my $list = "";
  if( defined($hash->{id}) ) {
    if( !$hash->{hub} ) {
      $hash->{hub} = harmony_hubOfDevice($hash->{id});

      return "no hub found for device $name ($hash->{id})" if( !$hash->{hub} );
    }

    if( $cmd ne "?" && !$param ) {
      $cmd = "PowerOn" if( $cmd eq "on" );
      $cmd = "PowerOff" if( $cmd eq "off" );

      my $device = harmony_deviceOfId( $defs{$hash->{hub}}, $hash->{id} );
      if( harmony_actionOfCommand( $device, $cmd ) ) {
        $param = $cmd;
        $cmd = "command";
      }
    }

    if( $cmd eq "command" ) {
      $param2 = $param;
      $param = $hash->{id};

      $hash = $defs{$hash->{hub}};

    } elsif( $cmd eq "hidDevice" || $cmd eq "text" || $cmd eq "cursor" || $cmd eq "special" || $cmd eq "hid" ) {
      my $id = $hash->{id};

      $hash = $defs{$hash->{hub}};

      my $device = harmony_deviceOfId( $hash, $id );
      return "unknown device" if( !$device );

      return "no keyboard associated with device $device->{label}" if( !$device->{IsKeyboardAssociated} );

      if( !$hash->{hidDevice} || $hash->{hidDevice} ne $id ) {
        $hash->{hidDevice} = $id;
        harmony_sendIq($hash, "<oa xmlns='connect.logitech.com' mime='harmony.engine?sethiddevice' token=''>deviceId=$id</oa>");
        sleep( 3 );
      }

      return if( $cmd eq "hidDevice" );

    } else {
      $list = "command hidDevice:noArg text cursor:up,down,left,right,pageUp,pageDown,home,end special:previousTrack,nextTrack,stop,playPause,volumeUp,volumeDown,mute";
      #my $device = harmony_deviceOfId( $defs{$hash->{hub}}, $hash->{id} );
      #$list .= " on off" if( !defined($device->{isManualPower}) || $device->{isManualPower} eq "false" || !$device->{isManualPower} );
      return "Unknown argument $cmd, choose one of $list";

    }
  }

  if( $cmd ne "?" && !$param ) {
    if( $cmd eq 'off' ) {
      $cmd = "activity";
      $param = "-1";
    } elsif( my $activity = harmony_activityOfId($hash, $hash->{currentActivityID}) ) {
      if( harmony_actionOfCommand( $activity, $cmd ) ) {
        $param = $cmd;
        $cmd = "command";
      }
    }
  }

  if( $cmd eq 'activity' ) {
    $param = harmony_idOfActivity($hash, $param) if( $param && $param !~ m/^([\d-])+$/ );
    return "unknown activity" if( !$param );

    harmony_sendEngineGet($hash, "startActivity", "activityId=$param:timestamp=0");

    delete $hash->{channelAfterStart};
    $hash->{channelAfterStart} = $param2 if( $param2 );

    return undef;
  } elsif( $cmd eq "channel" ) {
    delete $hash->{channelAfterStart};
    return "no current activity" if( !defined($hash->{currentActivityID}) || $hash->{currentActivityID} == -1 );

    my $activity = harmony_activityOfId($hash, $hash->{currentActivityID});
    return "no device with 'channel changing role' in current activity $activity->{label}" if( !$activity->{isTuningDefault} );

    return "missing channel" if( !$param );

    harmony_sendEngineGet($hash, "changeChannel", "channel=$param:timestamp=0");

    return undef;

  } elsif( $cmd eq "command" ) {
    my $action;

    if( !$param2 ) {
      return "unknown activity" if( !$hash->{currentActivityID} );
      return "unknown command" if( !$param );

      my $activity = harmony_activityOfId($hash, $hash->{currentActivityID});
      return "unknown activity" if( !$activity );

      $action = harmony_actionOfCommand( $activity, $param );
      return "unknown command $param" if( !$action );

    } else {
      $param = harmony_idOfDevice($hash, $param) if( $param && $param !~ m/^([\d-])+$/ );
      return "unknown device" if( !$param );
      return "unknown command" if( !$param2 );

      my $device = harmony_deviceOfId( $hash, $param );
      return "unknown device" if( !$device );

      $action = harmony_actionOfCommand( $device, $param2 );
      return "unknown command $param2" if( !$action );
    }

    my $duration = $param_h->{duration};
    return "duration musst be numeric" if( defined($duration) && $duration !~ m/^([\d.-])+$/ );
    $duration = 0.1 if( !$duration || $duration < 0 );
    $duration = 5 if $duration > 5;

    Log3 $name, 4, "$name: sending $action->{command} for ${duration}s for ". harmony_labelOfDevice($hash, $action->{deviceId} );

    my $payload = "status=press:action={'command'::'$action->{command}','type'::'$action->{type}','deviceId'::'$action->{deviceId}'}:timestamp=0";
    harmony_sendEngineRender($hash, "holdAction", $payload);
    select(undef, undef, undef, ($duration));
    $payload = "status=release:action={'command'::'$action->{command}','type'::'$action->{type}','deviceId'::'$action->{deviceId}'}:timestamp=".$duration*1000;
    harmony_sendEngineRender($hash, "holdAction", $payload);

    return undef;
  } elsif( $cmd eq "getCurrentActivity" ) {
    harmony_sendEngineGet($hash, "getCurrentActivity");

    return undef;

  } elsif( $cmd eq "getConfig" ) {
    harmony_sendEngineGet($hash, "config");

    return undef;

  } elsif( $cmd eq "hidDevice" ) {
    my $id = $param;

    if( !$id && $hash->{currentActivityID} ) {
      my $activity = harmony_activityOfId($hash, $hash->{currentActivityID});
      return "unknown activity" if( !$activity );

      return "no device with 'keyboard text entry role' in current activity $activity->{label}" if( !$activity->{KeyboardTextEntryActivityRole} );

      $id = $activity->{KeyboardTextEntryActivityRole};

    } else {
      $id = harmony_idOfDevice($hash, $id) if( $id && $id !~ m/^([\d-])+$/ );
      return "unknown device $param" if( $param && !$id );

      my $device = harmony_deviceOfId( $hash, $id );
      return "unknown device" if( !$device );

      return "no keyboard associated with device $device->{label}" if( !$device->{IsKeyboardAssociated} );

    }

    $hash->{hidDevice} = $id;
    harmony_sendIq($hash, "<oa xmlns='connect.logitech.com' mime='harmony.engine?sethiddevice' token=''>deviceId=$id</oa>");
    return undef;

  } elsif( $cmd eq "hid" || $cmd eq "text" || $cmd eq "cursor" || $cmd eq "special" ) {
    return "nothing to send" if( !$param );

    return "unknown activity" if( !$hash->{currentActivityID} );
    return "unknown command" if( !$param );

    if( !$hash->{hidDevice} ) {
      my $activity = harmony_activityOfId($hash, $hash->{currentActivityID});
      return "unknown activity" if( !$activity );

      return "no device with 'keyboard text entry role' in current activity $activity->{label}" if( !$activity->{KeyboardTextEntryActivityRole} );
    }

    if( $cmd eq "text" ) {
      $hash->{hid} = "" if( !$hash->{hid} );
      $hash->{hid} .= join(' ', @params);

      $param = undef;
    } elsif( $cmd eq "cursor" ) {
      $param = lc( $param ) if( $param );

      $param = "0700004A" if( $param eq "home" );
      $param = "0700004B" if( $param eq "pageup" );
      $param = "0700004D" if( $param eq "end" );
      $param = "0700004E" if( $param eq "pagedown" );
      $param = "0700004F" if( $param eq "right" );
      $param = "07000050" if( $param eq "left" );
      $param = "07000051" if( $param eq "down" );
      $param = "07000052" if( $param eq "up" );

      return "unknown cursor direction $param" if( $param !~ m/^07/ );

    } elsif( $cmd eq "special" ) {
      $param = lc( $param ) if( $param );

      $param = "01000081" if( $param eq "systempower" );
      $param = "01000082" if( $param eq "systemsleep" );
      $param = "01000083" if( $param eq "systemwake" );
      $param = "0C0000B5" if( $param eq "nexttrack" );
      $param = "0C0000B6" if( $param eq "previoustrack" );
      $param = "0C0000B7" if( $param eq "stop" );
      $param = "0C0000CD" if( $param eq "playpause" );
      $param = "0C0000E9" if( $param eq "volumeup" );
      $param = "0C0000EA" if( $param eq "volumedown" );
      $param = "0C0000E2" if( $param eq "mute" );

      return "unknown special key $param" if( $param !~ m/^0(1|C)/ );
    }

    harmony_sendHID($hash, $param);

    return undef;

  } elsif( $cmd eq "reconnect" ) {
    delete $hash->{helper}{UserAuthToken} if( $param && $param eq "all" );
    delete $hash->{identity} if( $param && $param eq "all" );
    harmony_connect($hash);

    return undef;

  } elsif( $cmd eq "autocreate" ) {
    return harmony_autocreate($hash,$param);

    return undef;

  } elsif( $cmd eq "sleeptimer" ) {
    my $interval = $param?$param*60:60*60;
    $interval = -1 if( $interval < 0 );

    harmony_sendIq($hash, "<oa xmlns='connect.logitech.com' mime='harmony.engine?setsleeptimer'>interval=$interval</oa>", "setsleeptimer");

    return undef;

  } elsif( $cmd eq "sync" ) {
    harmony_sendIq($hash, "<oa xmlns='connect.logitech.com' mime='setup.sync' token=''/>");

    return undef;

  } elsif( $cmd eq "update" ) {
    harmony_sendIq($hash, "<oa xmlns='connect.logitech.com' mime='vnd.logtech.setup/vnd.logitech.firmware?update' token=''>format=json</oa>");

    return undef;

  } elsif( $cmd eq "active" ) {
    return "can't activate disabled hub." if(AttrVal($name, "disable", undef));

    delete $hash->{protocol};
    $hash->{ConnectionState} = "Disconnected";
    readingsSingleUpdate( $hash, "state", $hash->{ConnectionState}, 1 );
    harmony_connect($hash);
    return undef;

  } elsif( $cmd eq "inactive" ) {
    harmony_disconnect($hash);
    readingsSingleUpdate($hash, "state", "inactive", 1);
    return undef;

  } elsif( $cmd eq "xxx" ) {
    #harmony_sendIq($hash, "<oa xmlns='connect.logitech.com' mime='vnd.logtech.setup/vnd.logitech.firmware?check' token=''>format=json</oa>");
    #harmony_sendIq($hash, "<oa xmlns='connect.logitech.com' mime='vnd.logtech.setup/vnd.logitech.firmware?status' token=''>format=json</oa>");
    #harmony_sendIq($hash, "<oa xmlns='connect.logitech.com' mime='vnd.logtech.setup/vnd.logitech.firmware?update' token=''>format=json</oa>");
    #harmony_sendIq($hash, "<oa xmlns='connect.logitech.com' mime='harmony.automation?notify' token=''></oa>");
    #harmony_sendIq($hash, "<oa xmlns='connect.logitech.com' mime='harmony.automation?getState' token=''></oa>");
    #harmony_sendIq($hash, "<oa xmlns='connect.logitech.com' mime='harmony.automation.state?notify' token=''></oa>");
    #harmony_sendIq($hash, "<query xmlns='jabber:iq:roster'/>");
    #harmony_sendIq($hash, "<query xmlns='http://jabber.org/protocol/disco#info'/>");
    #harmony_sendIq($hash, "<query xmlns='http://jabber.org/protocol/disco#items'/>");

    return undef;

  }

  if( $hash->{config} ) {
    return undef if( !defined($hash->{config}) );

    my $activities;
    if( $hash->{config}{activity} ) {
      foreach my $activity (sort { ($a->{activityOrder}||0) <=> ($b->{activityOrder}||0) } @{$hash->{config}{activity}}) {
        next if( $activity->{id} == -1 );
        $activities .= "," if( $activities );
        $activities .= $activity->{label};
       }
     }

    if( my $activity = harmony_activityOfId($hash, -1) ) {
      $activities .= "," if( $activities );
      $activities .= $activity->{label};
    }

    if( $activities ) {
      $activities =~ s/ /./g;

      $list .= " activity:$activities";
    }

    my $hidDevices;
    my $autocreateDevices;
    if( $hash->{config}->{device} ) {
      foreach my $device (sort { $a->{id} <=> $b->{id} } @{$hash->{config}->{device}}) {
        if( $device->{IsKeyboardAssociated} ) {
          $hidDevices .= "," if( $hidDevices );
          $hidDevices .= harmony_labelOfDevice($hash, $device->{id} );
        }

        if( !defined($modules{$hash->{TYPE}}{defptr}{$device->{id}}) ) {
          $autocreateDevices .= "," if( $autocreateDevices );
          $autocreateDevices .= harmony_labelOfDevice($hash, $device->{id} );
        }
      }
    }

    if( $hidDevices ) {
      $hidDevices =~ s/ /./g;

      $list .= " hidDevice:,$hidDevices";
    }

    if( $autocreateDevices ) {
      $autocreateDevices =~ s/ /./g;

      $list .= " autocreate:$autocreateDevices,";
    }

  }

  $list .= " channel" if( defined($hash->{currentActivityID}) && $hash->{currentActivityID} != -1 );

  $list .= " command active:noArg inactive:noArg getConfig:noArg getCurrentActivity:noArg off:noArg reconnect:noArg sleeptimer sync:noArg text cursor:up,down,left,right,pageUp,pageDown,home,end special:previousTrack,nextTrack,stop,playPause,volumeUp,volumeDown,mute";

  $list .= " update:noArg" if( $hash->{hubUpdate} );

  return "Unknown argument $cmd, choose one of $list";
}

sub
harmony_getLoginToken($)
{
  my ($hash) = @_;

  #return if( defined($hash->{helper}{UserAuthToken}) );

  if( 0 ) {
  my $https = "https";
  $https = "http" if( AttrVal($hash->{NAME}, "nossl", 0) );

  my $json = encode_json( { clientId => '',
                            clientTypeId => 'ControlApp' } );

  my($err,$data) = HttpUtils_BlockingGet({
    url => "$https://svcs.myharmony.com/discovery/Discovery.svc/json/GetJson2Uris",
    timeout => 10,
    #noshutdown => 1,
    #httpversion => "1.1",
    header => "Content-Type: application/json;charset=utf-8",
    data => $json,
  });

  harmony_dispatch( {hash=>$hash,type=>'GetJson2Uris'},$err,$data );
  }

  return if( defined($hash->{helper}{UserAuthToken}) );

  if( 1 || !$hash->{helper}{username} ) {
    $hash->{helper}{UserAuthToken} = "";
    return;

  }

  my $https = "https";
  $https = "http" if( AttrVal($hash->{NAME}, "nossl", 0) );

  my $json = encode_json( { email => harmony_decrypt($hash->{helper}{username}),
                            password => harmony_decrypt($hash->{helper}{password}) } );

  my($err,$data) = HttpUtils_BlockingGet({
    url => "$https://svcs.myharmony.com/CompositeSecurityServices/Security.svc/json/GetUserAuthToken",
    timeout => 10,
    #noshutdown => 1,
    #httpversion => "1.1",
    header => "Content-Type: application/json;charset=utf-8",
    data => $json,
  });

  harmony_dispatch( {hash=>$hash,type=>'token'},$err,$data );
}

sub
harmony_attr2hash($)
{
  my ($attr) = @_;

  my @args = split(' ', $attr);

  my %params = ();
  while (@args) {
    my $arg = shift(@args);

    my ($name,$value) = split("=", $arg,2);

    while( $value && $value =~ m/^'/ && $value !~ m/'$/ ) {
      my $next = shift(@args);
      last if( !defined($next) );
      $value .= " ". $next;
    }

    $params{$name} = substr( $value, 1, -1 );
  }

  return \%params;
}

sub
harmony_CDATA2hash($)
{
  my ($cdata) = @_;

  my @args = split(':', $cdata);

  my %params = ();
  while (@args) {
    my $arg = shift(@args);
    my ($name,$value) = split("=", $arg,2);

    #fix for updates=table: 0x...
    if( $args[0] && $args[0] !~ m/=/ ) {
      my $next = shift(@args);
      last if( !defined($next) );
      $value .= ":". $next;
    }

    ##fix for http://...
    #if( $args[0] && $args[0] =~ m/^\/\// ) {
    #  my $next = shift(@args);
    #  last if( !defined($next) );
    #  $value .= ":". $next;
    #}

    #fix for json {...<key>:<value>...}
    while( $value && $value =~ m/^{/ && $value !~ m/}$/ ) {
      my $next = shift(@args);
      last if( !defined($next) );
      $value .= ":". $next;
    }

    $params{$name} = $value if( $name );
  }

  return \%params;
}

use constant  { CTRL   => 0x01,
                SHIFT  => 0x02,
                ALT    => 0x04,
                GUI    => 0x08,
                RIGHT_CTRL  => 0x10,
                RIGHT_SHIFT => 0x20,
                RIGHT_ALT   => 0x40,
                RIGHT_GUI   => 0x80,
              };

my %keys = ( '1' => '0702001E',
             '2' => '0702001F',
             '3' => '07020020',
             '4' => '07020021',
             '5' => '07020022',
             '6' => '07020023',
             '7' => '07020024',
             '8' => '07020025',
             '9' => '07020026',
             '0' => '07020027',

            '\\n'=> '07000028',
            '\\e'=> '07000029',
            '\\t'=> '0700002B',
             ' ' => '0700002C',

             '!' => '0702001E',
             '"' => '0702001F',
             '§' => '07020020',
             '$' => '07020021',
             '%' => '07020022',
             '&' => '07020023',
             '/' => '07020024',
             '(' => '07020025',
             ')' => '07020026',
             '=' => '07020027',

             'ß' => '0700002D',
             '´' => '0700002E',
             'ü' => '0700002F',
             '+' => '07000030',
             '#' => '07000031',
             'ö' => '07000033',
             'ä' => '07000034',
             '<' => '07000035',
             ',' => '07000036',
             '.' => '07000037',
             '-' => '07000038',

             '?' => '0702002D',
             '`' => '0702002E',
             'Ü' => '0702002F',
             '*' => '07020030',
             "'" => '07020031',
             'Ö' => '07020033',
             'Ä' => '07020034',
             '>' => '07020035',
             ';' => '07020036',
             ':' => '07020037',
             '_' => '07020038',

           'F1'  => '0700003A',
           'F2'  => '0700003B',
           'F3'  => '0700003C',
           'F4'  => '0700003D',
           'F5'  => '0700003E',
           'F6'  => '0700003F',
           'F7'  => '07000040',
           'F8'  => '07000041',
           'F9'  => '07000042',
           'F10' => '07000043',
           'F11' => '07000044',
           'F12' => '07000045',

           'KP/' => '07000054',
           'KP*' => '07000055',
           'KP-' => '07000056',
           'KP+' => '07000057',
         'KP\\n' => '07000058',
           'KP1' => '07000059',
           'KP2' => '0700005A',
           'KP3' => '0700005C',
           'KP4' => '0700005C',
           'KP5' => '0700005D',
           'KP6' => '0700005E',
           'KP7' => '0700005F',
           'KP8' => '07000060',
           'KP9' => '07000061',
           'KP0' => '07000062',
         );


sub
harmony_char2hid($)
{
  my ($char) = @_;

  my $ret;
  if( $char ge '1' && $char le '9' ) {
    $ret = sprintf( "070000%02X", 0x1E + ord($char) - ord('1') );
  } elsif( $char ge 'a' && $char le 'z' ) {
    $ret = sprintf( "070000%02X", 0x04 + ord($char) - ord('a') );
  } elsif( $char ge 'A' && $char le 'Z' ) {
    $ret = sprintf( "070200%02X", 0x04 + ord($char) - ord('A') );
  } elsif( defined( $keys{$char} ) ) {
    $ret = $keys{$char};
  }

  return $ret;
}

sub
harmony_updateActivity($$;$)
{
  my ($hash,$id,$modifier) = @_;
  $modifier = "" if( !$modifier );

  if( $hash->{currentActivityID} && $hash->{currentActivityID}  ne $id ) {
    my $id = $hash->{currentActivityID};
    $hash->{previousActivityID} = $id;

    my $previous = harmony_labelOfActivity($hash,$id,$id);
    readingsSingleUpdate( $hash, "previousActivity", $previous, 0 );
  }

  if( !$modifier && defined($modules{$hash->{TYPE}}{defptr}) ) {
    if( my $activity = harmony_activityOfId($hash, $id)) {
      foreach my $id (keys %{$activity->{fixit}}) {
        if( my $hash = $modules{$hash->{TYPE}}{defptr}{$id} ) {
          my $state = $activity->{fixit}->{$id}->{Power};
          $state = "Manual" if( !$state );
          readingsSingleUpdate( $hash, "power", lc($state), 1 );
        }
      }
    }
  }

  $hash->{currentActivityID} = $id;

  my $activity = harmony_labelOfActivity($hash,$id,$id);
  readingsSingleUpdate( $hash, "currentActivity", "$modifier$activity", 1 );

  $activity =~ s/ /./g;
  if( !$modifier && $activity ne ReadingsVal($hash->{NAME},"activity", "" ) ) {
    readingsSingleUpdate( $hash, "activity", $activity, 1 );

    harmony_sendEngineGet($hash, "changeChannel", "channel=$hash->{channelAfterStart}:timestamp=0") if( $hash->{channelAfterStart} );
    delete $hash->{channelAfterStart};
  }

  delete $hash->{hidDevice} if( $id == -1 );
}

sub
harmony_Parse($$$)
{
  my ($hash, $content, $decoded) = @_;
  my $name = $hash->{NAME};
  my $ignored = 0;
  #Log 1, "harmony_Parse: >>>$content<<<";
  #Log 1, Dumper $decoded;

  if( !$decoded ) {
    #Log 1, "harmony_Parse: unhandled data >>>$content<<<";
  } elsif( $content =~ m/discoveryinfo\?get/ ) {
    Log3 $name, 4, "$name: discoveryinfo ";
    #Log3 $name, 4, "$name: ". Dumper $decoded;

    $hash->{discoveryinfo} = $decoded;

    #$hash->{current_fw_version} = $decoded->{current_fw_version} if( defined($decoded->{current_fw_version}) );

    harmony_sendEngineGet($hash, "config");

  } elsif( $content =~ m/\?config$/ ) {
    Log3 $name, 3, "$name: new config ";

    $hash->{config} = $decoded;

    harmony_sendIq($hash, "<oa xmlns='connect.logitech.com' mime='vnd.logitech.connect/vnd.logitech.statedigest?get' token=''>format=json</oa>");

  } elsif( $content =~ m/statedigest\?get$/
           || $content =~ m/stateDigest\?notify$/ ) {
    Log3 $name, 4, "$name: statedigest ";

    if( $decoded ) {
      if( defined($decoded->{syncStatus}) ) {
        harmony_sendEngineGet($hash, "config") if( $hash->{syncStatus} && !$decoded->{syncStatus} );

        $hash->{syncStatus} = $decoded->{syncStatus};
      }

      if( defined($decoded->{hubUpdate}) && $decoded->{hubUpdate} eq "true" && !$hash->{hubUpdate} ) {
        harmony_sendIq($hash, "<oa xmlns='connect.logitech.com' mime='vnd.logtech.setup/vnd.logitech.firmware?check' token=''>format=json</oa>");
      }

      $hash->{activityStatus} = $decoded->{activityStatus} if( defined($decoded->{activityStatus}) );

      $hash->{hubSwVersion} = $decoded->{hubSwVersion} if( defined($decoded->{hubSwVersion}) );
      $hash->{hubUpdate} = ($decoded->{hubUpdate} eq 'true'?1:0) if( defined($decoded->{hubUpdate}) );

      my $modifier = "";
      $modifier = "starting " if( $hash->{activityStatus} == 1 );
      $modifier = "stopping " if( $hash->{activityStatus} == 3 );

      harmony_updateActivity($hash, $decoded->{activityId}, $modifier) if( defined($decoded->{activityId}) );

      if( defined($decoded->{sleepTimerId}) ) {
        if( $decoded->{sleepTimerId} == -1 ) {
          delete $hash->{sleeptimer};
          DoTrigger( $name, "sleeptimer: expired" );
        } else {
          harmony_sendEngineGet($hash, "gettimerinterval", "timerId=$decoded->{sleepTimerId}");
        }
      }
    }

  } elsif( $content =~ m/harmony.engine\?startActivity$/ ) {
    Log3 $name, 4, "$name: startActivity ";

    my $done = $decoded->{done};
    my $total = $decoded->{total};
    my $id = $decoded->{deviceId};

    $id = "<unknown>" if( !defined($id) );

    my $label = harmony_labelOfDevice($hash,$id,$id);
    $label = "<unknown>" if( !defined($label) );

    if( $done == $total ) {
      Log3 $name, 4, "$name: done starting/stopping device: $label";
    } elsif( $done == 1  ) {
      Log3 $name, 4, "$name: starting/stopping device: $label";
    } else {
      Log3 $name, 4, "$name: starting/stopping device ($done/$total): $label";
    }

  } elsif( $content =~ m/harmony.engine\?startActivityFinished$/ ) {
    Log3 $name, 4, "$name: startActivityFinished ";

    if( my $id = $decoded->{activityId} ) {
     if( harmony_activityOfId($hash, $id) ) {
       if( $id == -1 && $hash->{helper}{ignorePowerOff} ) {
         delete $hash->{helper}{ignorePowerOff};

       } else {
         harmony_updateActivity($hash, $id);
       }

     } else {
       $hash->{helper}{ignorePowerOff} = 1;

     }
   }

  } elsif( $content =~ m/engine\?gettimerinterval$/ ) {
    $hash->{sleeptimer} = FmtDateTime( gettimeofday() + $decoded->{interval} );
    DoTrigger( $name, "sleeptimer: $hash->{sleeptimer}" );

  } elsif( $content =~ m/engine\?holdAction$/ ) {
     $ignored = 1;

  } elsif( $content =~ m/engine\?setsleeptimer$/ ) {
     $ignored = 1;

  } elsif( $content =~ m/automation.state/ ) {
     $ignored = 1;

  } else {
    Log3 $name, 4, "harmony_Parse: unhandled data >>>$content<<<";
    Log3 $name, 5, Dumper $decoded;
  }

  if( $ignored ) {
    Log3 $name, 4, "harmony_Parse: ignored data >>>$content<<<";
    Log3 $name, 5, Dumper $decoded;
  }
}

sub
harmony_Read($)
{
  my ($hash) = @_;
  my $name = $hash->{NAME};

  if( $hash->{sendSocket} ) {
    my @clientinfo = $hash->{CD}->accept();
    if( !@clientinfo ) {
      Log3 $name, 1, "Accept failed ($name: $!)" if($! != EAGAIN);
      return undef;
    }
    $hash->{CONNECTS}++;

    my ($port, $iaddr) = sockaddr_in($clientinfo[1]);
    my $caddr = inet_ntoa($iaddr);
    Log3 $name, 3, "$name: new discovery response from $caddr";

    my $len;
    my $buf;

    $len = sysread( $clientinfo[0], $buf, 10240 );
    close $clientinfo[0];
    return if( !defined($len) || !$len );

    Log3 $name, 5, "$name: $buf";

    my @args = split(';', $buf);

    my %params = ();
    while( @args ) {
      my $arg = shift(@args);

      my ($name,$value) = split(":", $arg,2);
      $params{$name} = $value;
    }
    Log3 $name, 4, Dumper \%params;

    #DoTrigger( $name, "DISCOVERED $hash->{TYPE}:$params{remoteId} type=$hash->{TYPE} alias='$params{friendlyName}' name='".lc($params{friendlyName})."' detail=$params{ip} define='define <name> <TYPE> $params{ip}'" );

    $hash->{helper}{discovered}{$params{remoteId}} = \%params;

    foreach my $chash ( values %{$modules{$hash->{TYPE}}{defptr}} ) {
      next if( $chash->{NAME} eq 'harmony:discovery' );
      next if( !defined($chash->{ip}) );
      next if( $chash->{remoteId} );
      if( $chash->{ip} eq $params{ip} ) {
        $chash->{discoveryinfo} = \%params;
        $chash->{remoteId} = $chash->{discoveryinfo}{remoteId};

        Log3 $name, 4, Dumper  $chash->{discoveryinfo}{protocolVersion};
        if( $chash->{discoveryinfo}{protocolVersion} =~ m/XMPP/ ) {
          delete $chash->{remoteId} if( !AttrVal( $chash->{NAME}, 'forceWebSocket', 0 ) );
        }

        harmony_connect( $chash );
      }
    }

    return;
  }

  if( $hash->{remoteId} ) {
#Log 1, "harmony_Read";
    my $len;
    my $buf;

    $len = sysread( $hash->{CD}, $buf, 10240 );
#Log 1, $buf;

    my $close = 0;
    if( !defined($len) || !$len ) {
      $close = 1;

    } elsif( $hash->{websocket} ) {
      $hash->{helper}{PARTIAL} .= $buf;

      do {
        my $fin  = (ord(substr($hash->{helper}{PARTIAL},0,1)) & 0x80)?1:0;
        my $op   = (ord(substr($hash->{helper}{PARTIAL},0,1)) & 0x0F);
        my $mask = (ord(substr($hash->{helper}{PARTIAL},1,1)) & 0x80)?1:0;
        my $len  = (ord(substr($hash->{helper}{PARTIAL},1,1)) & 0x7F);
        my $i = 2;

#Log 1, $len;
        if( $len == 126 ) {
          $len = unpack( 'n', substr($hash->{helper}{PARTIAL},$i,2) );
          $i += 2;
        } elsif( $len == 127 ) {
          $len = unpack( 'N', substr($hash->{helper}{PARTIAL},$i+4,6) );
          $i += 8;
        }

        if( $mask ) {
          $mask = substr($hash->{helper}{PARTIAL},$i,4);
          $i += 4;
        }
#Log 1, "$fin $op $mask $len";
        return if( $len > length($hash->{helper}{PARTIAL})-$i );

        my $data = substr($hash->{helper}{PARTIAL}, $i, $len);
        $hash->{helper}{PARTIAL} = substr($hash->{helper}{PARTIAL},$i+$len);

        if( $op == 0x01 ) {
          my $json = harmony_decode_json($data);
          my $decoded = $json;

          if( $json->{type} ) {
            harmony_Parse( $hash, $json->{type}, $json->{data} );

          } else {
            Log3 $name, 3, "no type: >>>$data<<<";

          }
        } elsif( $op == 0x0a ) {
          #ignore pong
        } else {
          Log3 $name, 4, "unhandled websocket payload type $op";
        }


      } while( $hash->{helper}{PARTIAL} && !$close );

    } elsif( $buf =~ m'^HTTP/1.1 101 Switching Protocols'i )  {
      $hash->{websocket} = 1;
      #buf = harmony_msg2hash($buf, 1);

      Log3 $name, 3, "$name: websocket: Switching Protocols ok";

      harmony_sendEngineGet($hash, "config");
      harmony_sendIq($hash, "<oa xmlns='connect.logitech.com' mime='connect.discoveryinfo?get'>format=json</oa>");

      RemoveInternalTimer($hash);
      InternalTimer(gettimeofday()+50, "harmony_ping", $hash, 0);

    } else {
      $close = 1;
      Log3 $name, 2, "$name: websocket: Switching Protocols failed";
    }

    if( $close ) {
      harmony_disconnect( $hash );

      if( defined($hash->{discoveryinfo}) ) {
        InternalTimer(gettimeofday()+2, "harmony_connect", $hash, 0);
      }
    }

    return;
  }

  my $buf;
  my $ret = sysread( $hash->{CD}, $buf, 1024*1024 );

  if(!defined($ret) || $ret <= 0) {
    harmony_disconnect( $hash );

    InternalTimer(gettimeofday()+2, "harmony_connect", $hash, 0);
    return;
  }


  my $data = $hash->{helper}{PARTIAL};
  $data .= $buf;

  if( $hash->{fakeOrigin} ) {
    if( $data =~ m/200 OK/ ) {
      if( $data =~ m/^(.*?)\r?\n\r?\n(.*)$/s ) {
        my $header = $1;
        my $body = $2;

        if( my $json = harmony_decode_json($body) ) {
          Log3 $name, 3, "$name: answer for fake origin query received";
          $hash->{discoveryinfo} = $json->{data};
          $hash->{remoteId} = $hash->{discoveryinfo}{remoteId};

          Log3 $name, 4, Dumper  $hash->{discoveryinfo}{protocolVersion};
          if( $hash->{discoveryinfo}{protocolVersion} =~ m/XMPP/ ) {
            delete $hash->{remoteId} if( !AttrVal( $hash->{NAME}, 'forceWebSocket', 0 ) );
          }

          harmony_connect( $hash );
        }

      } else {
        $hash->{helper}{PARTIAL} = $data;
      }

    } else {
      harmony_tryXMPP($hash);

    }

    return;
  }

  #FIXME: should use real xmpp/xml parser
  # see forum https://forum.fhem.de/index.php/topic,14163.msg575033.html#msg575033
  $data =~ s/<iq\/>//g;
  $data =~ s/<\/iq><iq/<\/iq>\n<iq/g;
  my @lines = split( "\n", $data );
  foreach my $line (@lines) {
    if( $line =~ m/^<(\w*)\s*([^>]*)?\/>(.*)?/ ) {
      Log3 $name, 5, "$name: tag: $1, attr: $2";

      $data = $3;

      if( $line eq "<success xmlns='urn:ietf:params:xml:ns:xmpp-sasl'/>" ) {
        $hash->{ConnectionState} = "LoggedIn";

        if( $hash->{helper}{UserAuthToken} ) {
          harmony_getSessionToken($hash);
        } else {
          #harmony_sendIq($hash, "<oa xmlns='connect.logitech.com' mime='vnd.logitech.connect/vnd.logitech.ping?get' token=''></oa>");
          #harmony_sendIq($hash, "<oa xmlns='connect.logitech.com' mime='vnd.logitech.harmony/vnd.logitech.harmony.system?systeminfo' token=''></oa>");
          #harmony_sendIq($hash, "<oa xmlns='connect.logitech.com' mime='vnd.logitech.connect/vnd.logitech.deviceinfo?get' token=''></oa>");
          #harmony_sendIq($hash, "<oa xmlns='connect.logitech.com' mime='vnd.logitech.setup/vnd.logitech.account?getProvisionInfo' token=''></oa>");
          #harmony_sendIq($hash, "<oa xmlns='connect.logitech.com' mime='vnd.logitech.connect/vnd.logitech.statedigest?get' token=''>format=json</oa>");
          #harmony_sendIq($hash, "<oa xmlns='connect.logitech.com' mime='vnd.logtech.setup/vnd.logitech.firmware?check' token=''>format=json</oa>");
          #harmony_sendIq($hash, "<oa xmlns='connect.logitech.com' mime='vnd.logtech.setup/vnd.logitech.firmware?status' token=''>format=json</oa>");
          #harmony_sendIq($hash, "<oa xmlns='connect.logitech.com' mime='vnd.logtech.setup/vnd.logitech.firmware?update' token=''>format=json</oa>");
          #harmony_sendIq($hash, "<oa xmlns='connect.logitech.com' mime='proxy.resource?get' token=''>hetag= :uri=dynamite:://HomeAutomationService/Config/:encode=true</oa>");
          #harmony_sendIq($hash, "<oa xmlns='connect.logitech.com' mime='harmony.automation?getstate' token=''></oa>");

          #harmony_sendIq($hash, "<oa xmlns='connect.logitech.com' mime='vnd.logitech.connect/vnd.logitech.pair'>name=1vm7ATw/tN6HXGpQcCs/A5MkuvI#iOS6.0.1#iPhone</oa>");

          harmony_sendIq($hash, "<oa xmlns='connect.logitech.com' mime='connect.discoveryinfo?get'>format=json</oa>");
          #harmony_sendEngineGet($hash, "config");
        }

        RemoveInternalTimer($hash);
        InternalTimer(gettimeofday()+50, "harmony_ping", $hash, 0);
      }

      $line = $3;
    }

    if( $line =~ m/^<(\w*)([^>]*)>(.*)<\/\1>(.*)?/ ) {
      Log3 $name, 5, "$name: tag: $1, attr: $2";
      #Log3 $name, 5, "  data: $3";

      $data = $4;

      my $tag = $1;
      my $attr = $2;
      my $content = $3;

      #if( $content =~ m/^<(\w*)([^>]*)>(.*)<\/\1>(.*)?/ ) {
      #  Log3 $name, 1, "$name: tag: $1, attr: $2";
      #  Log3 $name, 1, Dumper harmony_attr2hash($2);
      #}

      if( $content =~ m/<!\[CDATA\[(.*)\]\]>/ ) {
        my $cdata = $1;

        my $json;
        my $decoded;
        if( $cdata =~ m/^{.*}$/ ) {
          $json = harmony_decode_json($cdata);
          $decoded = $json;

        } else {
          $decoded = harmony_CDATA2hash($cdata);

        }

        my $error = $decoded->{errorCode};
        if( $error && $error != 200 ) {
            Log3 $name, 2, "$name: error ($error): $decoded->{errorString}";
        }

        if( ($tag eq "iq" &&  $content =~ m/statedigest\?get'/)
            || ($tag eq "message" && $content =~ m/type="connect.stateDigest\?notify"/) ) {
          Log3 $name, 4, "$name: statedigest: $cdata";

          if( $decoded ) {
            if( defined($decoded->{syncStatus}) ) {
              harmony_sendEngineGet($hash, "config") if( $hash->{syncStatus} && !$decoded->{syncStatus} );

              $hash->{syncStatus} = $decoded->{syncStatus};
            }

            if( defined($decoded->{hubUpdate}) && $decoded->{hubUpdate} eq "true" && !$hash->{hubUpdate} ) {
              harmony_sendIq($hash, "<oa xmlns='connect.logitech.com' mime='vnd.logtech.setup/vnd.logitech.firmware?check' token=''>format=json</oa>");
            }

            $hash->{activityStatus} = $decoded->{activityStatus} if( defined($decoded->{activityStatus}) );

            $hash->{hubSwVersion} = $decoded->{hubSwVersion} if( defined($decoded->{hubSwVersion}) );
            $hash->{hubUpdate} = ($decoded->{hubUpdate} eq 'true'?1:0) if( defined($decoded->{hubUpdate}) );

            my $modifier = "";
            $modifier = "starting " if( $hash->{activityStatus} == 1 );
            $modifier = "stopping " if( $hash->{activityStatus} == 3 );

            harmony_updateActivity($hash, $decoded->{activityId}, $modifier) if( defined($decoded->{activityId}) );

            if( defined($decoded->{sleepTimerId}) ) {
              if( $decoded->{sleepTimerId} == -1 ) {
                delete $hash->{sleeptimer};
                DoTrigger( $name, "sleeptimer: expired" );
              } else {
                harmony_sendEngineGet($hash, "gettimerinterval", "timerId=$decoded->{sleepTimerId}");
              }
            }
          }
        } elsif( $tag eq "message" ) {

          if( $content =~ m/type="harmony.engine\?startActivityFinished"/ ) {
            if( my $id = $decoded->{activityId} ) {
              if( harmony_activityOfId($hash, $id) ) {
                if( $id == -1 && $hash->{helper}{ignorePowerOff} ) {
                  delete $hash->{helper}{ignorePowerOff};

                } else {
                  harmony_updateActivity($hash, $id);
                }

              } else {
                $hash->{helper}{ignorePowerOff} = 1;

              }
            }

          } elsif( $content =~ m/type="vnd.logitech.harmony\/vnd.logitech.control.button\?pressType"/ ) {
            DoTrigger( $name, "vnd.logitech.control.button: $decoded->{type}" );

          } elsif( $content =~ m/type="automation.state\?notify"/ ) {
            DoTrigger( $name, "automation.state: $cdata" );

          } else {
            Log3 $name, 4, "$name: unknown message: $content";

          }

        } elsif( $tag eq "iq" ) {
          if( $content =~ m/errorcode='(\d*)'.*errorstring='(.*)'/ && $1 != 100 && $1 != 200 ) {
            Log3 $name, 2, "$name: error ($1): $2";

          } elsif( $content =~ m/vnd.logitech.pair/ ) {

            if( !$hash->{identity} && $decoded->{identity} ) {
              $hash->{identity} = $decoded->{identity};
              harmony_connect($hash);

            } else {
              harmony_sendIq($hash, "<oa xmlns='connect.logitech.com' mime='connect.discoveryinfo?get'>format=json</oa>");
              #harmony_sendEngineGet($hash, "config");

            }

          } elsif( $content =~ m/\?startactivity/i ) {
            if( $cdata =~ m/done=(\d*):total=(\d*)(:deviceId=(\d*))?/ ) {
              my $done = $1;
              my $total = $2;
              my $id = $4;

              $id = "<unknown>" if( !defined($id) );

              my $label = harmony_labelOfDevice($hash,$id,$id);

              if( $done == $total ) {
                Log3 $name, 4, "$name: done starting/stopping device: $label";
              } elsif( $done == 1  ) {
                Log3 $name, 4, "$name: starting/stopping device: $label";
              } else {
                Log3 $name, 4, "$name: starting/stopping device ($done/$total): $label";
              }

            } else {
              Log3 $name, 3, "$name: unknown startactivity message: $content";
            }

          } elsif( $content =~ m/discoveryinfo\?get/ && $decoded ) {
            #Log3 $name, 4, "$name: ". Dumper $decoded;

            $hash->{discoveryinfo} = $decoded;

            #$hash->{current_fw_version} = $decoded->{current_fw_version} if( defined($decoded->{current_fw_version}) );

            harmony_sendEngineGet($hash, "config");

          } elsif( $content =~ m/engine\?changeChannel/ && $decoded ) {

          } elsif( $content =~ m/engine\?gettimerinterval/ && $decoded ) {
            $hash->{sleeptimer} = FmtDateTime( gettimeofday() + $decoded->{interval} );
            DoTrigger( $name, "sleeptimer: $hash->{sleeptimer}" );

          } elsif( $content =~ m/firmware\?/ && $decoded ) {
            Log3 $name, 4, "$name: firmware: $cdata";

            if( $decoded->{status} && $decoded->{newVersion} ) {
              $hash->{newVersion} = $decoded->{newVersion};

              my $txt = $decoded->{newVersion};
              $txt .= ", isCritical: $decoded->{isCritical}";
              $txt .= ", bytes: $decoded->{totalBytes}";

              readingsSingleUpdate( $hash, "newVersion", $txt, 1 ) if( $txt ne ReadingsVal($hash->{NAME},"newVersion", "" ) );

            } else {
              delete $hash->{newVersion};

            }

          } elsif( $content =~ m/\?config/ && $decoded ) {
            $hash->{config} = $decoded;
            Log3 $name, 3, "$name: new config ";
            #Log3 $name, 5, "$name: ". Dumper $json;

            #my $station = $hash->{config}->{content}->{contentImageHost};
            #$station =~ s/{stationId}/4faa0c3b7232c50c26001b86/;
            #harmony_sendIq($hash, "<oa xmlns='connect.logitech.com' mime='proxy.resource?get' token=''>hetag= :uri=content:://1.0/user;$station:encode=true</oa>");

            #foreach my $device (sort { $a->{id} <=> $b->{id} } @{$hash->{config}->{device}}) {
            #  my $content = $hash->{config}->{content}->{contentDeviceHost};
            #  $content =~ s/{deviceProfileUri}/$device->{deviceProfileUri}/;
            #  harmony_sendIq($hash, "<oa xmlns='connect.logitech.com' mime='proxy.resource?get' token=''>hetag= :uri=content:://1.0/user;$content:encode=true</oa>");
            #  last;
            #}

            #harmony_sendIq($hash, "<oa xmlns='connect.logitech.com' mime='proxy.resource?get' token=''>hetag= :uri=content:://1.0/user;$hash->{config}->{content}->{householdUserProfileUri}:encode=true</oa>");

            harmony_sendIq($hash, "<oa xmlns='connect.logitech.com' mime='vnd.logitech.connect/vnd.logitech.statedigest?get' token=''>format=json</oa>");
            #harmony_sendEngineGet($hash, "getCurrentActivity");

          } elsif( $cdata =~ m/result=(.*)/ ) {
            my $result = $1;
            Log3 $name, 4, "$name: got result $1";

            if( $content =~ m/getCurrentActivity/ ) {
              harmony_updateActivity($hash, $result);

            } else {
              Log3 $name, 3, "$name: unknown result: $content";

            }

          } elsif( $content =~ m/mime='hid.report'/ ) {
            harmony_sendHID($hash) if( $hash->{hid} );

          } else {
            Log3 $name, 3, "$name: unknown iq: $content";
Log 3, Dumper $decoded;

Log 3, Dumper harmony_decode_json($decoded->{resource}) if( !$json && $decoded->{resource} && $decoded->{resource} =~ m/^{.*}$/ );

          }

        } else {
          Log3 $name, 3, "$name: unhandled tag: $line";

        }

      } elsif( $content =~ m/mime='hid.report'/ ) {
        harmony_sendHID($hash) if( $hash->{hid} );

      } elsif( $line =~ m/<iq id='ping-(\d+)' type='result'><\/iq>/ ) {
        Log3 $name, 5, "$name: got ping response $1";

      } elsif( $line ) {
        Log3 $name, 4, "$name: unknown (no cdata): $line";

      }
    } elsif( $line =~ m/^<\?xml.*id='([\w-]*).*error.*>/ ) {
      Log3 $name, 2, "$name: error: $1" if( $1 );
      Log3 $name, 4, "$name: $line";

      harmony_disconnect($hash);

    } elsif( $line =~ m/^<\?xml.*PLAIN.*>/ ) {

      my $identity = $hash->{identity}?$hash->{identity}:"guest";
      my $auth = encode_base64("\0$identity\@connect.logitech.com\0gatorade.",'');
      harmony_send($hash, "<auth xmlns='urn:ietf:params:xml:ns:xmpp-sasl' mechanism='PLAIN'>$auth</auth>");

      $data = "";

    } elsif( $line =~ m/^<.*>$/ ) {
      Log3 $name, 4, "$name: unknown: $line";

    } elsif( $line ) {
      #Log3 $name, 5, "$name: $line";

    }
  }

  $hash->{helper}{PARTIAL} = $data;
#Log 3, "length: ". length($hash->{helper}{PARTIAL});
}

sub
harmony_hash2header($)
{
  my ($hash) = @_;

  return $hash if( ref($hash) ne 'HASH' );

  my $header;
  foreach my $key (keys %{$hash}) {
    #$header .= "\r\n" if( $header );
    $header .= "$key: $hash->{$key}\r\n";
  }

  return $header;
}

sub
harmony_disconnect($)
{
  my ($hash) = @_;
  my $name = $hash->{NAME};

  RemoveInternalTimer($hash);
  $hash->{ConnectionState} = "Disconnected";
  readingsSingleUpdate( $hash, "state", $hash->{ConnectionState}, 1 ) if( $hash->{ConnectionState} ne ReadingsVal($name, "state", "" ) );

  delete $hash->{websocket};
  delete $hash->{fakeOrigin};
  $hash->{helper}{PARTIAL} = "";

  return if( !$hash->{CD} );
  Log3 $name, 2, "$name: disconnect";

  close($hash->{CD}) if($hash->{CD});
  delete($hash->{FD});
  delete($hash->{CD});
  delete($selectlist{$name});

  $hash->{LAST_DISCONNECT} = FmtDateTime( gettimeofday() );
}

sub
harmony_connect($)
{
  my ($hash) = @_;
  my $name = $hash->{NAME};

  return if( IsDisabled($name) );
  return if( !defined($hash->{ip}) );

  harmony_disconnect($hash);

  Log3 $name, 4, "$name: connect";
  if( $hash->{remoteId} ) {
    my $timeout = $hash->{TIMEOUT} ? $hash->{TIMEOUT} : 2;
    if( my $socket = IO::Socket::INET->new(PeerAddr=>"$hash->{ip}:8088", Timeout=>$timeout) ) {
      Log3 $name, 3, "$name: connected";
      $hash->{protocol} = "WEBSOCKET";
      $hash->{ConnectionState} = "Connected";
      readingsSingleUpdate( $hash, "state", $hash->{ConnectionState}, 1 ) if( $hash->{ConnectionState} ne ReadingsVal($name, "state", "" ) );
      $hash->{LAST_CONNECT} = FmtDateTime( gettimeofday() );

      $hash->{FD}    = $socket->fileno();
      $hash->{CD}    = $socket;         # sysread / close won't work on fileno
      $hash->{CONNECTS}++;
      $selectlist{$name} = $hash;

      my $domain = "svcs.myharmony.com";
      if( $hash->{discoveryinfo}{discoveryServerUri} =~ m'https://([^/]+)' ) {
        $domain = $1;
      }
      my $ret = "GET /?domain=$domain&hubId=$hash->{remoteId} HTTP/1.1\r\n";
      $ret .= harmony_hash2header( {                    'Host' => "$hash->{ip}:8088",
                                                     'Upgrade' => 'websocket',
                                                  'Connection' => 'Upgrade',
                                                      'Pragma' => 'no-cache',
                                               'Cache-Control' => 'no-cache',
                                           'Sec-WebSocket-Key' => 'RkhFTQ==',
                                       'Sec-WebSocket-Version' => '13',
                                  } );

      $ret .= "\r\n";
      Log3 $name, 5, "$name: $ret";

      syswrite( $hash->{CD}, $ret );

    } else {
      harmony_disconnect( $hash );

      InternalTimer(gettimeofday()+10, "harmony_connect", $hash, 0);
    }
    return;
  }

  harmony_getLoginToken($hash);

  if( defined($hash->{helper}{UserAuthToken}) ) {
    my $timeout = $hash->{TIMEOUT} ? $hash->{TIMEOUT} : 3;
    my $conn = IO::Socket::INET->new(PeerAddr => "$hash->{ip}:5222", Timeout => $timeout);

    if( $conn ) {
      Log3 $name, 3, "$name: connected";
      $hash->{protocol} = "XMPP";
      $hash->{ConnectionState} = "Connected";
      readingsSingleUpdate( $hash, "state", $hash->{ConnectionState}, 1 ) if( $hash->{ConnectionState} ne ReadingsVal($name, "state", "" ) );
      $hash->{LAST_CONNECT} = FmtDateTime( gettimeofday() );

      $hash->{FD}    = $conn->fileno();
      $hash->{CD}    = $conn;         # sysread / close won't work on fileno
      $hash->{CONNECTS}++;
      $selectlist{$name} = $hash;

      harmony_send($hash, "<stream:stream to='connect.logitech.com' xmlns:stream='http://etherx.jabber.org/streams' xmlns='jabber:client' xml:lang='en' version='1.0'>");

    } else {
      harmony_disconnect( $hash );

      InternalTimer(gettimeofday()+10, "harmony_connect", $hash, 0);
    }
  }
}

sub
harmony_getSessionToken($)
{
  my ($hash) = @_;
  my $name = $hash->{NAME};

  my $unique_id = '1vm7ATw/tN6HXGpQcCs/A5MkuvI';
  my $device = 'iOS6.0.1#iPhone';

  harmony_sendPair($hash, "token=$hash->{helper}{UserAuthToken}:name=$unique_id#$device");
}

sub
harmony_send($$)
{
  my ($hash, $data) = @_;
  my $name = $hash->{NAME};

  return undef if( !$hash->{CD} );

  Log3 $name, 4, "$name: send: $data";

  if( $hash->{websocket} ) {
    if( $data =~ m/mime='([^']*)'/ ) {
      my $cmd = $1;
      my $params;
      if( $data =~ m/>([^<]+)/ ) {
       $params = harmony_CDATA2hash($1);
       if( $params && defined($params->{action}) ) {
          Log3 $name, 5, Dumper $params->{action};
          $params->{action} =~ s/'/"/g;
          $params->{action} =~ s/::/:/g;
          $params->{verb} =  "render";
          Log3 $name, 5, Dumper $params->{action};
       }
       $params = encode_json( $params );
       Log3 $name, 4, "cmd: $params";
      }
      Log3 $name, 4, "cmd: $cmd";

      my $txt = '{ "hbus": { "cmd": "'. $cmd .'" } }';
      if( $params ) {
        $txt = '{ "hbus": { "cmd": "'. $cmd .'","params":'. $params .' } }';
      }
      Log3 $name, 4, "txt: $txt";

      my $len = length($txt);
      if( $len < 126 ) {
        $txt = chr(0x81) . chr($len) . $txt;
      } else {
        if ( $len < 65536 ) {
          $txt = chr(0x81) . chr(0x7E) . pack('n', $len) . $txt;
        } else {
          $txt = chr(0x81) . chr(0x7F) . chr(0x00) . chr(0x00) .
                 chr(0x00) . chr(0x00) . pack('N', $len) . $txt;
        }
      }

      syswrite( $hash->{CD}, $txt );
    }
    return;
  }

  syswrite $hash->{CD}, $data;
}
my $id = 0;
sub
harmony_sendIq($$;$)
{
  my ($hash, $xml, $type) = @_;
  $type = 'get' if ( !$type );

  ++$id;

  my $iq = "<iq type='$type' id='$id' from='guest'>$xml</iq>";
  $iq = "<iq type='$type' id='$id'>$xml</iq>";

  harmony_send($hash,$iq);
}
sub
harmony_sendPair($$)
{
  my ($hash, $payload) = @_;
  $payload = '' if ( !$payload );

  my $xml = "<oa xmlns='connect.logitech.com' mime='vnd.logitech.connect/vnd.logitech.pair'>$payload</oa>";

  harmony_sendIq($hash,$xml);
}
sub
harmony_sendEngineGet($$;$)
{
  my ($hash, $endpoint, $payload) = @_;
  $payload = '' if ( !$payload );

  my $xml = "<oa xmlns='connect.logitech.com' mime='vnd.logitech.harmony/vnd.logitech.harmony.engine?$endpoint'>$payload</oa>";

  harmony_sendIq($hash,$xml);
}
sub
harmony_sendHID($;$)
{
  my ($hash, $code) = @_;

  if( !$code ) {
    return if( !$hash->{hid} );

    my $char = substr($hash->{hid}, 0, 1);
    $hash->{hid} = substr($hash->{hid}, 1);

    if( $char eq '\\' || ord($char) == 0xC3 ) {
      $char .= substr($hash->{hid}, 0, 1);
      $hash->{hid} = substr($hash->{hid}, 1);
    }

    $code = harmony_char2hid( $char );
  }

  my $xml = "<oa xmlns='connect.logitech.com' mime='hid.report' token=''>{'code':'$code'}</oa>";

  harmony_sendIq($hash,$xml);
}
sub
harmony_sendEngineRender($$$)
{
  my ($hash, $endpoint, $payload) = @_;

  #my $xml = "<oa xmlns='connect.logitech.com' mime='vnd.logitech.harmony/vnd.logitech.harmony.engine?$endpoint' token=''>$payload</oa>";
  my $xml = "<oa xmlns='connect.logitech.com' mime='vnd.logitech.harmony/vnd.logitech.harmony.engine?$endpoint'>$payload</oa>";

  harmony_sendIq($hash,$xml, "render");
}

sub
harmony_ping($)
{
  my( $hash ) = @_;

  return if( $hash->{ConnectionState} eq "Disconnected" );

  if( $hash->{remoteId} ) {
    my $txt = chr(0x89) . chr(0);
    syswrite( $hash->{CD}, $txt );

    RemoveInternalTimer($hash);
    InternalTimer(gettimeofday()+50, "harmony_ping", $hash, 0);
    return;
  }

  ++$id;
  harmony_send($hash, "<iq type='get' id='ping-$id'><ping xmlns='urn:xmpp:ping'/></iq>");

  RemoveInternalTimer($hash);
  InternalTimer(gettimeofday()+50, "harmony_ping", $hash, 0);
}


sub
harmony_dispatch($$$)
{
  my ($param, $err, $data) = @_;
  my $hash = $param->{hash};
  my $name = $hash->{NAME};

  if( $err ) {
    Log3 $name, 2, "$name: http request failed: $err";
  } elsif( $data ) {
    Log3 $name, 4, "$name: $data";

    if( $data !~ m/^{.*}$/ ) {
      Log3 $name, 2, "$name: invalid json detected: $data";
      return undef;
    }

    my $json = harmony_decode_json($data);

    if( $param->{type} eq 'token' ) {
      harmony_parseToken($hash, $json);

    } elsif( $param->{type} eq 'GetJson2Uris' ) {
Log 1, Dumper $json;

    }
  }
}

sub
harmony_autocreate($;$)
{
  my($hash, $param) = @_;
  my $name = $hash->{NAME};

  return if( !defined($hash->{config}) );

  my $id = $param;
  $id = harmony_idOfDevice($hash, $id) if( $id && $id !~ m/^([\d-])+$/ );
  return "unknown device $param" if( $param && !$id );

  #foreach my $d (keys %defs) {
  #  next if($defs{$d}{TYPE} ne "autocreate");
  #  return undef if( IsDisabled($defs{$d}{NAME} ) );
  #}

  my $autocreated = 0;
  foreach my $device (@{$hash->{config}->{device}}) {
    next if( $id && $device->{id} != $id );

    if( defined($modules{$hash->{TYPE}}{defptr}{$device->{id}}) ) {
      Log3 $name, 4, "$name: hramony device for '$device->{id}' already defined";
      next;
    }

    my $devname = "harmony_". $device->{id};
    my $define = "$devname harmony DEVICE $device->{id}";

    Log3 $name, 3, "$name: create new device '$devname' for device '$device->{id}'";
    my $cmdret = CommandDefine(undef,$define);
    if($cmdret) {
      Log3 $name, 1, "$name: Autocreate: An error occurred while creating device for id '$device->{id}': $cmdret";

    } else {
      $cmdret = CommandAttr(undef,"$devname alias $device->{label}") if( defined($device->{label}) );
      $cmdret = CommandAttr(undef,"$devname event-on-change-reading .*");
      $cmdret = CommandAttr(undef,"$devname room $hash->{TYPE}");
      $cmdret = CommandAttr(undef,"$devname stateFormat power");
      #$cmdret = CommandAttr(undef,"$devname IODev $name");

      $autocreated++;
    }
  }

  CommandSave(undef,undef) if( $autocreated && AttrVal( "autocreate", "autosave", 1 ) );

  return "created $autocreated devices";
}

sub
harmony_parseToken($$)
{
  my($hash, $json) = @_;
  my $name = $hash->{NAME};

  RemoveInternalTimer($hash);

  my $error = $json->{ErrorCode};
  if( $error && $error != 200 ) {
    Log3 $name, 2, "$name: error ($error): $json->{Message}";
    $hash->{lastError} = $json->{Message};
  }

  my $had_token = $hash->{helper}{UserAuthToken};

  $hash->{helper}{AccountId} = $json->{GetUserAuthTokenResult}->{AccountId};
  $hash->{helper}{UserAuthToken} = $json->{GetUserAuthTokenResult}->{UserAuthToken};

  if( $hash->{helper}{UserAuthToken} ) {
    $hash->{ConnectionState} = "GotToken";

  } else {
    $hash->{STATE} = "Error";
    $hash->{ConnectionState} = "Error";

    RemoveInternalTimer($hash);
    InternalTimer(gettimeofday()+60, "harmony_connect", $hash, 0);

  }
}

sub
harmony_data2string($)
{
  my ($data) = @_;

   return "" if( !defined($data) );

   return $data if( !ref($data) );
   return $data if( ref($data) =~ m/JSON::..::Boolean/ );
   return "[". join(',', @{$data}) ."]" if(ref($data) eq "ARRAY");

   return Dumper $data;
}
sub
harmony_GetPower($$)
{
  my ($hash, $activity) = @_;

  my $power = "";
  return $power if( !defined($activity->{fixit}) );

  foreach my $id (keys %{$activity->{fixit}}) {
    my $label = harmony_labelOfDevice($hash, $id);
    my $state = $activity->{fixit}->{$id}->{Power};
    $state = "Manual" if( !$state );

    $power .= "\n\t\t\t$label: $state";
  }

  return $power;
}
sub
harmony_Get($$@)
{
  my ($hash, $name, $cmd, @params) = @_;
  my ($param) = @params;
  #$cmd = lc( $cmd );

  my $list = "";

  if( $hash->{sendSocket} ) {
    my $list = "discovered:noArg";

    if( $cmd eq "discovered" ) {
      my $ret;
      foreach my $remoteId ( sort keys %{$hash->{helper}{discovered}}) {
        my $discoveryinfo = $hash->{helper}{discovered}{$remoteId};
        $ret .= "\n" if( $ret );
        $ret .= sprintf( "%-8s %-15s %-20s %-9s %s", $discoveryinfo->{remoteId}, $discoveryinfo->{ip}, $discoveryinfo->{friendlyName}, $discoveryinfo->{current_fw_version}, $discoveryinfo->{protocolVersion} );
      }
      $ret = sprintf( "%-8s %-15s %-20s %-9s %s\n", "remoteId", "ip", "friendlyName", "fw_vers", "protocolVersion" ).$ret if( $ret );
      $ret .= "\n" if( $ret );

      return $ret;
    }

    return "Unknown argument $cmd, choose one of $list";
  }

  if( defined($hash->{id}) ) {
    if( !$hash->{hub} ) {
      $hash->{hub} = harmony_hubOfDevice($hash->{id});

      return "no IODev found for device $name ($hash->{id})" if( !$hash->{hub} );
    }

    if( $cmd eq "commands" || $cmd eq "deviceCommands" ) {
      $cmd = "deviceCommands";
      $param = $hash->{id};

      $hash = $defs{$hash->{hub}};

    } else {
      $list = "commands:noArg";
      return "Unknown argument $cmd, choose one of $list";

    }

  }


  my $ret;
  if( $cmd eq "activities" ) {
    return "no activities found" if( !defined($hash->{config}) || !defined($hash->{config}{activity}) );

    my $ret = "";
    foreach my $activity (sort { ($a->{activityOrder}||0) <=> ($b->{activityOrder}||0) } @{$hash->{config}{activity}}) {
      next if( $activity->{id} == -1  );
      $ret .= "\n" if( $ret );
      $ret .= sprintf( "%s\t%-24s", $activity->{id}, $activity->{label});
      foreach my $param (@params) {
        $ret .= "\t". harmony_data2string($activity->{$param}) if( $param && defined($activity->{$param}) );
      }

      if( $param && $param eq "power" ) {
        my $power = harmony_GetPower($hash, $activity);
        $ret .= $power if( $power );
      }
    }
    if( my $activity = harmony_activityOfId($hash, -1) ) {
      $ret .= "\n-1\t\t$activity->{label}";
      if( $param && $param eq "power" ) {
        my $power = harmony_GetPower($hash, $activity);
        $ret .= $power if( $power );
      }
    }
    #$ret = sprintf("%s\t\t%-24s\n", "ID", "LABEL"). $ret if( $ret );

    return $ret;

  } elsif( $cmd eq "devices" ) {
    return "no devices found" if( !defined($hash->{config}) || !defined($hash->{config}->{device}) );

    my $ret = "";
    foreach my $device (sort { $a->{id} <=> $b->{id} } @{$hash->{config}->{device}}) {
      $ret .= "\n" if( $ret );
      $ret .= sprintf( "%s\t%-20s\t%-20s\t%-15s\t%-15s", $device->{id}, $device->{label}, $device->{type}, $device->{manufacturer}, $device->{model});
      foreach my $param (@params) {
        $ret .= "\t". harmony_data2string($device->{$param}) if( $param && defined($device->{$param}) );
      }
    }
    #$ret = sprintf("%s\t\t%-20s\t%-20s\t%-15s\t%-15s\n", "ID", "LABEL", "TYPE", "MANUFACTURER", "MODEL"). $ret if( $ret );
    return $ret;

  } elsif( $cmd eq "commands" ) {
    return "no commands found" if( !defined($hash->{config}) || !defined($hash->{config}{activity}) );

    my $id = $param;
    $id = harmony_idOfActivity($hash, $id) if( $id && $id !~ m/^([\d-])+$/ );
    return "unknown activity $param" if( $param && !$id );

    my $ret = "";

    foreach my $activity (sort { ($a->{activityOrder}||0) <=> ($b->{activityOrder}||0) } @{$hash->{config}{activity}}) {
      next if( $activity->{id} == -1 );
      next if( $id && $activity->{id} != $id );
      $ret .= "$activity->{label}\n";
      #$ret .= "$device->{label}\t$device->{manufacturer}\t$device->{model}\n";
      foreach my $group (@{$activity->{controlGroup}}) {
        $ret .= "\t$group->{name}\n";
        foreach my $function (@{$group->{function}}) {
          my $action = harmony_decode_json($function->{action});

          $ret .= sprintf( "\t\t%-20s\t%s (%s)\n", $function->{name}, $function->{label}, harmony_labelOfDevice($hash, $action->{deviceId}, $action->{deviceId}) );
        }
      }
    }

    return $ret;

  } elsif( $cmd eq "deviceCommands" ) {
    return "no commands found" if( !defined($hash->{config}) || !defined($hash->{config}->{device}) );

    my $id = $param;
    $id = harmony_idOfDevice($hash, $id) if( $id && $id !~ m/^([\d-])+$/ );
    return "unknown device $param" if( $param && !$id );

    my $ret = "";

    if( $hash->{config}{device} ) {
      foreach my $device (sort { $a->{id} <=> $b->{id} } @{$hash->{config}->{device}}) {
        next if( $id && $device->{id} != $id );
        $ret .= "$device->{label}\t$device->{manufacturer}\t$device->{model}\n";
        foreach my $group (@{$device->{controlGroup}}) {
          $ret .= "\t$group->{name}\n";
          foreach my $function (@{$group->{function}}) {
            $ret .= sprintf( "\t\t%-20s\t%s\n", $function->{name}, $function->{label} );
          }
        }
      }
    }

    return "no commands found" if( !$ret );
    return $ret;

  } elsif( $cmd eq "activityDetail"
           || $cmd eq "deviceDetail" ) {
    return undef if( !defined($hash->{config}) );

    $param = harmony_idOfActivity($hash, $param) if( $param && $param !~ m/^([\d-])+$/ && $cmd eq "activityDetail" );
    $param = harmony_idOfDevice($hash, $param) if( $param && $param !~ m/^([\d-])+$/ && $cmd eq "deviceDetail" );

    my $var;
    $var = $hash->{config}{activity} if( $cmd eq "activityDetail"  );
    $var = $hash->{config}{device}   if( $cmd eq "deviceDetail"  );
    if( $param ) {
      foreach my $v (@{$var}) {
        if( $v->{id} eq $param ) {
          $var = $v;
          last;
        }
      }
    }

    return Dumper $var;

  } elsif( $cmd eq "configDetail" ) {

    return undef if( !defined($hash->{config}) );

    return Dumper $hash->{config};

  } elsif( $cmd eq "currentActivity" ) {
      return "unknown activity" if( !$hash->{currentActivityID} );

      my $activity = harmony_activityOfId($hash, $hash->{currentActivityID});
      return "unknown activity" if( !$activity );

      return $activity->{label};
  }

  $list .= "activities:noArg devices:noArg";

  if( $hash->{config} ) {
    return undef if( !defined($hash->{config}) );

    my $activities;
    if( $hash->{config}{activity} ) {
      foreach my $activity (sort { ($a->{activityOrder}||0) <=> ($b->{activityOrder}||0) } @{$hash->{config}{activity}}) {
        next if( $activity->{id} == -1 );
        $activities .= "," if( $activities );
        $activities .= $activity->{label};
      }
    }
    if( $activities ) {
      $activities =~ s/ /./g;

      $list .= " commands:,$activities";
    }

    my $devices;
    if( $hash->{config}{device} ) {
      foreach my $device (sort { $a->{id} <=> $b->{id} } @{$hash->{config}->{device}}) {
        $devices .= "," if( $devices );
        $devices .= $device->{label};
      }
    }
    if( $devices ) {
      $devices =~ s/ /./g;

      $list .= " deviceCommands:,$devices";
    }
  }

  if( $cmd eq 'showAccount' ) {
    my $user = $hash->{helper}{username};
    my $password = $hash->{helper}{password};

    return 'no user set' if( !$user );
    return 'no password set' if( !$password );

    $user = harmony_decrypt( $user );
    $password = harmony_decrypt( $password );

    return "user: $user\npassword: $password";
  }
  $list .= " showAccount";

  $list .= " currentActivity:noArg";

  $list =~ s/^ //;
  return "Unknown argument $cmd, choose one of $list";
}

sub
harmony_Attr($$$)
{
  my ($cmd, $name, $attrName, $attrVal) = @_;

  my $orig = $attrVal;

  if( $attrName eq "disable" ) {
    my $hash = $defs{$name};
    RemoveInternalTimer($hash);
    if( $cmd eq "set" && $attrVal ne "0" ) {
      $attrVal = 1;
      harmony_disconnect($hash);

    } else {
      $attr{$name}{$attrName} = 0;
      harmony_connect($hash);

    }
  }

  if( $cmd eq "set" ) {
    if( !defined($orig) || $orig ne $attrVal ) {
      $attr{$name}{$attrName} = $attrVal;
      return $attrName ." set to ". $attrVal;
    }
  }

  return;
}



sub
harmony_encrypt($)
{
  my ($decoded) = @_;
  my $key = getUniqueId();
  my $encoded;

  return $decoded if( $decoded =~ /^crypt:(.*)/ );

  for my $char (split //, $decoded) {
    my $encode = chop($key);
    $encoded .= sprintf("%.2x",ord($char)^ord($encode));
    $key = $encode.$key;
  }

  return 'crypt:'. $encoded;
}
sub
harmony_decrypt($)
{
  my ($encoded) = @_;
  my $key = getUniqueId();
  my $decoded;

  $encoded = $1 if( $encoded =~ /^crypt:(.*)/ );

  for my $char (map { pack('C', hex($_)) } ($encoded =~ /(..)/g)) {
    my $decode = chop($key);
    $decoded .= chr(ord($char)^ord($decode));
    $key = $decode.$key;
  }

  return $decoded;
}


1;

=pod
=item summary    module for logitech harmony hub based remots
=item summary_DE Modul für Logitech Harmony Hub basierte Fernbedienungen
=begin html

<a name="harmony"></a>
<h3>harmony</h3>
<ul>
  Defines a device to integrate a Logitech Harmony Hub based remote control into fhem.<br><br>

  It is possible to: start and stop activities, send ir commands to devices, send keyboard input by bluetooth and
  smart keyboard usb dongles.<br><br>

  You probably want to use it in conjunction with the <a href="#fakeRoku">fakeRoku</a> module.<br><br>

  Notes:
  <ul>
    <li>JSON has to be installed on the FHEM host.</li>
    <li>For hubs with firmware version 3.x.y &lt;username&gt; and &lt;password&gt; are not required as no authentication
    with the logitech myharmony server is needed for the full functionality of this module.</li>
    <li>For hubs with firmware version 4.x.y &lt;username&gt; and &lt;password&gt; are required for device level control.
    Activit level control is (currently) still possible without authentication.</li>
    <li>activity and device names can be given as id or name. names can be given as a regex and spaces in names musst be replaced by a single '.' (dot).</li>
  </ul><br>

  <a name="harmony_Define"></a>
  <b>Define</b>
  <ul>
    <code>define &lt;name&gt; harmony [&lt;username&gt; &lt;password&gt;] &lt;ip&gt;</code><br>
    <br>

    Defines a harmony device.<br><br>

    Examples:
    <ul>
      <code>define hub harmony 10.0.1.4</code><br>
    </ul>
  </ul><br>

  <a name="harmony_Readings"></a>
  <b>Readings</b>
  <ul>
    <li>currentActivity<br>
      the name of the currently selected activity.</li>
    <li>previousActivity<br>
      the name of the previous selected activity. does not trigger an event.</li>
    <li>newVersion<br>
      will be set if a new firmware version is avaliable.</li>
  </ul><br>

  <a name="harmony_Internals"></a>
  <b>Internals</b>
  <ul>
    <li>currentActivityID<br>
      the id of the currently selected activity.</li>
    <li>previousActivityID<br>
      the id of the previous selected activity.</li>
    <li>sleeptimer<br>
      timeout for sleeptimer if any is set.</li>
  </ul><br>


  <a name="harmony_Set"></a>
  <b>Set</b>
  <ul>
    <li>activity &lt;id&gt;|&lt;name&gt; [&lt;channel&gt;]<br>
      switch to this activit and optionally switch to &lt;channel&gt;</li>
    <li>channel &lt;channel&gt;<br>
      switch to &lt;channel&gt; in the current activity</li>
    <li>command [&lt;id&gt;|&lt;name&gt;] &lt;command&gt; [duration=&lt;duration&gt;]<br>
      send the given ir command for the current activity or for the given device</li>
    <li>getConfig<br>
      request the configuration from the hub</li>
    <li>getCurrentActivity<br>
      request the current activity from the hub</li>
    <li>off<br>
      switch current activity off</li>
    <li>reconnect [all]<br>
      close connection to the hub and reconnect, if <code>all</code> is given also reconnect to the logitech server</li>
    <li>sleeptimer [&lt;timeout&gt;]<br>
      &lt;timeout&gt; -> timeout in minutes<br>
      -1 -> timer off<br>
      default -> 60 minutes</li>
    <li>sync<br>
      syncs the hub to the myHarmony config</li>
    <li>hidDevice [&lt;id&gt;|&lt;name&gt;]<br>
      sets the target device for keyboard commands, if no device is given -> set the target to the
      default device for the current activity.</li>
    <li>text &lt;text&gt;<br>
      sends &lt;text&gt; by bluetooth/smart keaboard dongle. a-z ,A-Z ,0-9, \n, \e, \t and space are currently possible</li>
    <li>cursor &lt;direction&gt;<br>
      moves the cursor by bluetooth/smart keaboard dongle. &lt;direction&gt; can be one of: up, down, left, right, pageUp, pageDown, home, end.</li>
    <li>special &lt;key&gt;<br>
      sends special key by bluetooth/smart keaboard dongle. &lt;key&gt; can be one of: previousTrack, nextTrack, stop, playPause, volumeUp, volumeDown, mute.</li>
    <li>autocreate [&lt;id&gt;|&lt;name&gt;]<br>
      creates a fhem device for a single/all device(s) in the harmony hub. if activities are startet the state
      of these devices will be updatet with the power state defined in these activites.</li>
    <li>update<br>
      triggers a firmware update. only available if a new firmware is available.</li>
    <li>inactive<br>
      inactivates the current device. note the slight difference to the
      disable attribute: using set inactive the state is automatically saved
      to the statefile on shutdown, there is no explicit save necesary.<br>
      this command is intended to be used by scripts to temporarily
      deactivate the harmony device.<br>
      the concurrent setting of the disable attribute is not recommended.</li>
    <li>active<br>
      activates the current device (see inactive).</li>
  </ul>
  The command, hidDevice, text, cursor and special commmands are also available for the autocreated devices. The &lt;id&gt;|&lt;name&gt; paramter hast to be omitted.<br><br>

  <a name="harmony_Get"></a>
  <b>Get</b>
  <ul>
    <li>activites [&lt;param&gt;]<br>
      lists all activities<br>
      parm = power -> list power state for each device in activity</li>
    <li>devices [&lt;param&gt;]<br>
      lists all devices</li>
    <li>commands [&lt;id&gt;|&lt;name&gt;]<br>
      lists the commands for the specified activity or for all activities</li>
    <li>deviceCommands [&lt;id&gt;|&lt;name&gt;]<br>
      lists the commands for the specified device or for all devices</li>
    <li>activityDetail [&lt;id&gt;|&lt;name&gt;]</li>
    <li>deviceDetail [&lt;id&gt;|&lt;name&gt;]</li>
    <li>configDetail</li>
    <li>currentActivity<br>
      returns the current activity name</li>
    <li>showAccount<br>
      display obfuscated user and password in cleartext</li>
  </ul>
  The commands commmand is also available for the autocreated devices. The &lt;id&gt;|&lt;name&gt; paramter hast to be omitted.<br><br>


  <a name="harmony_Attr"></a>
  <b>Attributes</b>
  <ul>
    <li>forceWebSocket<br>
      1 -> use websocket interface even if xmpp availability is dicovered</li>
    <li>disable<br>
      1 -> disconnect from the hub</li>
  </ul>
</ul>

=end html
=cut
