###############################################################################
#
# Developed with VSCodium and richterger perl plugin.
#
#  (c) 2017-2023 Copyright: Marko Oldenburg (fhemdevelopment at cooltux dot net)
#  All rights reserved
#
#   Special thanks goes to comitters:
#       - Vitolinker / Commandref
#
#
#  This script is free software; you can redistribute it and/or modify
#  it under the terms of the GNU General Public License as published by
#  the Free Software Foundation; either version 2 of the License, or
#  any later version.
#
#  The GNU General Public License can be found at
#  http://www.gnu.org/copyleft/gpl.html.
#  A copy is found in the textfile GPL.txt and important notices to the license
#  from the author is found in LICENSE.txt distributed with these scripts.
#
#  This script is distributed in the hope that it will be useful,
#  but WITHOUT ANY WARRANTY; without even the implied warranty of
#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#  GNU General Public License for more details.
#
#
# $Id: LGTVWebOS.pm 28182 2023-11-17 20:26:24Z CoolTux $
#
###############################################################################
package FHEM::Devices::LGTV::LGTVWebOS;

use strict;
use warnings;
use experimental qw /switch/;

## try / catch
use Try::Tiny;

# use Carp;
use autodie qw /:io/;
##

use FHEM::Meta;
use GPUtils qw(GP_Import);

#-- Run before package compilation
BEGIN {
    #-- Export to main context with different name
    GP_Import(
        qw(
          modules
          init_done
          selectlist
          defs
        )
    );
}

my $missingModul = "";

eval { require MIME::Base64;     1 } or $missingModul .= 'MIME::Base64 ';
eval { require IO::Socket::INET; 1 } or $missingModul .= 'IO::Socket::INET ';
eval { use IO::Socket::SSL;      1 } or $missingModul .= 'IO::Socket::SSL ';

## no critic (Conditional "use" statement. Use "require" to conditionally include a module (Modules::ProhibitConditionalUseStatements))
eval { use Digest::SHA qw /sha1_hex/; 1 } or $missingModul .= 'Digest::SHA ';
eval { use Encode qw /encode_utf8 decode_utf8/; 1 }
  or $missingModul .= 'Encode ';
## use critic

eval { require Blocking; 1 } or $missingModul .= 'Blocking ';

# try to use JSON::MaybeXS wrapper
#   for chance of better performance + open code
eval {
    require JSON::MaybeXS;
    import JSON::MaybeXS qw( decode_json encode_json );
    1;
} or do {

    # try to use JSON wrapper
    #   for chance of better performance
    eval {
        # JSON preference order
        local $ENV{PERL_JSON_BACKEND} =
          'Cpanel::JSON::XS,JSON::XS,JSON::PP,JSON::backportPP'
          unless ( defined( $ENV{PERL_JSON_BACKEND} ) );

        require JSON;
        import JSON qw( decode_json encode_json );
        1;
    } or do {

        # In rare cases, Cpanel::JSON::XS may
        #   be installed but JSON|JSON::MaybeXS not ...
        eval {
            require Cpanel::JSON::XS;
            import Cpanel::JSON::XS qw(decode_json encode_json);
            1;
        } or do {

            # In rare cases, JSON::XS may
            #   be installed but JSON not ...
            eval {
                require JSON::XS;
                import JSON::XS qw(decode_json encode_json);
                1;
            } or do {

                # Fallback to built-in JSON which SHOULD
                #   be available since 5.014 ...
                eval {
                    require JSON::PP;
                    import JSON::PP qw(decode_json encode_json);
                    1;
                } or do {

                    # Fallback to JSON::backportPP in really rare cases
                    require JSON::backportPP;
                    import JSON::backportPP qw(decode_json encode_json);
                    1;
                };
            };
        };
    };
};

my %lgCommands = (

    "getServiceList"        => ["ssap://api/getServiceList"],
    "getChannelList"        => ["ssap://tv/getChannelList"],
    "getVolume"             => ["ssap://audio/getVolume"],
    "getAudioStatus"        => ["ssap://audio/getStatus"],
    "getCurrentChannel"     => ["ssap://tv/getCurrentChannel"],
    "getChannelProgramInfo" => ["ssap://tv/getChannelProgramInfo"],
    "getForegroundAppInfo"  =>
      ["ssap://com.webos.applicationManager/getForegroundAppInfo"],
    "getAppList"   => ["ssap://com.webos.applicationManager/listApps"],
    "getAppStatus" => ["ssap://com.webos.service.appstatus/getAppStatus"],
    "getExternalInputList" => ["ssap://tv/getExternalInputList"],
    "get3DStatus" => ["ssap://com.webos.service.tv.display/get3DStatus"],
    "powerOff"    => ["ssap://system/turnOff"],
    "powerOn"     => ["ssap://system/turnOn"],
    "3DOn"        => ["ssap://com.webos.service.tv.display/set3DOn"],
    "3DOff"       => ["ssap://com.webos.service.tv.display/set3DOff"],
    "volumeUp"    => ["ssap://audio/volumeUp"],
    "volumeDown"  => ["ssap://audio/volumeDown"],
    "channelDown" => ["ssap://tv/channelDown"],
    "channelUp"   => ["ssap://tv/channelUp"],
    "play"        => ["ssap://media.controls/play"],
    "stop"        => ["ssap://media.controls/stop"],
    "pause"       => ["ssap://media.controls/pause"],
    "rewind"      => ["ssap://media.controls/rewind"],
    "fastForward" => ["ssap://media.controls/fastForward"],
    "closeViewer" => ["ssap://media.viewer/close"],
    "closeApp"    => ["ssap://system.launcher/close"],
    "openApp"     => ["ssap://system.launcher/open"],
    "closeWebApp" => ["ssap://webapp/closeWebApp"],
    "openChannel" => [ "ssap://tv/openChannel",         "channelNumber" ],
    "launchApp"   => [ "ssap://system.launcher/launch", "id" ],
    "screenMsg"   => [ "ssap://system.notifications/createToast", "message" ],
    "mute"        => [ "ssap://audio/setMute",                    "mute" ],
    "volume"      => [ "ssap://audio/setVolume",                  "volume" ],
    "switchInput" => [ "ssap://tv/switchInput",                   "input" ],
);

my %openApps = (
    'Maxdome'            => 'maxdome',
    'AmazonLovefilm'     => 'lovefilm.de',
    'AmazonVideo'        => 'amazon',
    'YouTube'            => 'youtube.leanback.v4',
    'Netflix'            => 'netflix',
    'TV'                 => 'com.webos.app.livetv',
    'GooglePlay'         => 'googleplaymovieswebos',
    'Browser'            => 'com.webos.app.browser',
    'Chili.tv'           => 'Chilieu',
    'TVCast'             => 'de.2kit.castbrowsing',
    'Smartshare'         => 'com.webos.app.smartshare',
    'Scheduler'          => 'com.webos.app.scheduler',
    'Miracast'           => 'com.webos.app.miracast',
    'TVGuide'            => 'com.webos.app.tvguide',
    'Timemachine'        => 'com.webos.app.timemachine',
    'ARDMediathek'       => 'ard.mediathek',
    'Arte'               => 'com.3827031.168353',
    'WetterMeteo'        => 'meteonews',
    'Notificationcenter' => 'com.webos.app.notificationcenter',
    'Plex'               => 'cdp-30',
    'SkyOnline'          => 'de.sky.skyonline',
    'Smart-IPTV'         => 'com.1827622.109556',
    'Spotify'            => 'spotify-beehive',
    'DuplexIPTV'         => 'com.duplexiptv.app',
    'Disney+'            => 'com.disney.disneyplus-prod',
    'Smart-IPTV'         => 'siptv',
    'AppleTV'            => 'com.apple.appletv',
    'Joyn'               => 'joyn',
    'YouTube-Kids'       => 'youtube.leanback.kids.v4',
    'DAZN'               => 'dazn',
    'SkyQ'               => 'com.skygo.app.de.q',
    'WaipuTv'            => 'tv.waipu.app.waipu-lg',
);

my %openAppsPackageName = reverse %openApps;

sub Define {
    my $hash = shift;
    my $def  = shift;
    my $version;

    return $@ unless ( FHEM::Meta::SetInternals($hash) );

    $version = FHEM::Meta::Get( $hash, 'version' );
    our $VERSION = $version;

    my @a = split( "[ \t][ \t]*", $def );

    return "too few parameters: define <name> LGTV_WebOS <HOST>" if ( @a != 3 );
    return
      "Cannot define LGTV_WebOS device. Perl modul ${missingModul} is missing."
      if ($missingModul);

    my $name = $a[0];
    my $host = $a[2];

    $hash->{HOST}    = $host;
    $hash->{VERSION} = version->parse($VERSION)->normal;
    $hash->{PARTIAL} = '';
    $hash->{helper}{device}{channelguide}{counter} = 0;
    $hash->{helper}{device}{registered}            = 0;
    $hash->{helper}{device}{runsetcmd}             = 0;

    ::Log3( $name, 3, "LGTV_WebOS ($name) - defined with host $host" );

    ::CommandAttr( undef,
        $name . ' devStateIcon on:10px-kreis-gruen:off off:10px-kreis-rot:on' )
      if ( ::AttrVal( $name, 'devStateIcon', 'none' ) eq 'none' );
    ::CommandAttr( undef, $name . ' room LGTV' )
      if ( ::AttrVal( $name, 'room', 'none' ) eq 'none' );

    ::CommandDeleteReading( undef, $name . ' presence' )
      if ( ::AttrVal( $name, 'pingPresence', 0 ) == 0 );

    $modules{LGTV_WebOS}{defptr}{ $hash->{HOST} } = $hash;

    ::readingsSingleUpdate( $hash, 'state', 'Initialized', 1 );

    if ($init_done) {
        TimerStatusRequest($hash);
    }
    else {
        ::InternalTimer( ::gettimeofday() + 15,
            \&FHEM::Devices::LGTV::LGTVWebOS::TimerStatusRequest, $hash );
    }

    $hash->{helper}->{lastResponse} =
      int( ::gettimeofday() );    # Check Socket KeepAlive

    return;
}

sub Undef {
    my $hash = shift;
    my $arg  = shift;

    my $host = $hash->{HOST};
    my $name = $hash->{NAME};

    ::RemoveInternalTimer($hash);
    delete $modules{LGTV_WebOS}{defptr}{ $hash->{HOST} };
    ::Log3( $name, 3, "LGTV_WebOS ($name) - device $name deleted" );

    return;
}

sub Attr {
    my ( $cmd, $name, $attrName, $attrVal ) = @_;
    my $hash = $defs{$name};

    my $orig = $attrVal;

    if ( $attrName eq "disable" ) {
        if ( $cmd eq "set" && $attrVal eq "1" ) {
            ::RemoveInternalTimer($hash);
            ::readingsSingleUpdate( $hash, "state", "disabled", 1 );
            $hash->{PARTIAL} = '';
            ::Log3( $name, 3, "LGTV_WebOS ($name) - disabled" );
        }

        elsif ( $cmd eq "del" ) {
            ::readingsSingleUpdate( $hash, "state", "active", 1 );
            ::Log3( $name, 3, "LGTV_WebOS ($name) - enabled" );
            TimerStatusRequest($hash);
        }
    }

    if ( $attrName eq "disabledForIntervals" ) {
        if ( $cmd eq "set" ) {
            ::Log3( $name, 3,
                "LGTV_WebOS ($name) - enable disabledForIntervals" );
            ::readingsSingleUpdate( $hash, "state", "Unknown", 1 );
        }

        elsif ( $cmd eq "del" ) {
            ::readingsSingleUpdate( $hash, "state", "active", 1 );
            ::Log3( $name, 3,
                "LGTV_WebOS ($name) - delete disabledForIntervals" );
        }
    }

    return;
}

sub TimerStatusRequest {
    my $hash = shift;
    my $name = $hash->{NAME};

    ::RemoveInternalTimer( $hash, \&TimerStatusRequest );

    ::readingsBeginUpdate($hash);

    if (   !::IsDisabled($name)
        && $hash->{CD}
        && $hash->{helper}{device}{registered} == 1 )
    {

        ::Log3( $name, 4, "LGTV_WebOS ($name) - run get functions" );

        Presence($hash)
          if ( ::AttrVal( $name, 'pingPresence', 0 ) == 1 );

        if (   $hash->{helper}{device}{channelguide}{counter} > 2
            && ::AttrVal( $name, 'channelGuide', 0 ) == 1
            && ::ReadingsVal( $name, 'launchApp', 'TV' ) eq 'TV' )
        {

            GetChannelProgramInfo($hash);
            $hash->{helper}{device}{channelguide}{counter} = 0;

        }
        else {

            GetAudioStatus($hash);
            ::InternalTimer( ::gettimeofday() + 2,
                \&FHEM::Devices::LGTV::LGTVWebOS::GetCurrentChannel, $hash )
              if ( ::ReadingsVal( $name, 'launchApp', 'TV' ) eq 'TV' );
            ::InternalTimer( ::gettimeofday() + 4,
                \&FHEM::Devices::LGTV::LGTVWebOS::GetForgroundAppInfo, $hash );
            ::InternalTimer( ::gettimeofday() + 6,
                \&FHEM::Devices::LGTV::LGTVWebOS::Get3DStatus, $hash );
            ::InternalTimer( ::gettimeofday() + 8,
                \&FHEM::Devices::LGTV::LGTVWebOS::GetExternalInputList, $hash );
        }

    }
    elsif ( ::IsDisabled($name) ) {

        Close($hash);
        Presence($hash)
          if ( ::AttrVal( $name, 'pingPresence', 0 ) == 1 );
        $hash->{helper}{device}{runsetcmd} = 0;
        ::readingsBulkUpdateIfChanged( $hash, 'state', 'disabled' );

    }
    else {
        ::readingsBulkUpdateIfChanged( $hash, 'state', 'off' )
          if ( ::ReadingsVal( $name, 'state', 'off' ) ne 'off' );
        ::readingsBulkUpdateIfChanged( $hash, 'power', 'off' )
          if ( ::ReadingsVal( $name, 'power', 'off' ) ne 'off' );

        Presence($hash)
          if ( ::AttrVal( $name, 'pingPresence', 0 ) == 1 );

        ::readingsBulkUpdateIfChanged( $hash, 'channel',                 '-' );
        ::readingsBulkUpdateIfChanged( $hash, 'channelName',             '-' );
        ::readingsBulkUpdateIfChanged( $hash, 'channelMedia',            '-' );
        ::readingsBulkUpdateIfChanged( $hash, 'channelCurrentTitle',     '-' );
        ::readingsBulkUpdateIfChanged( $hash, 'channelCurrentStartTime', '-' );
        ::readingsBulkUpdateIfChanged( $hash, 'channelCurrentEndTime',   '-' );
        ::readingsBulkUpdateIfChanged( $hash, 'channelNextTitle',        '-' );
        ::readingsBulkUpdateIfChanged( $hash, 'channelNextStartTime',    '-' );
        ::readingsBulkUpdateIfChanged( $hash, 'channelNextEndTime',      '-' );

        $hash->{helper}{device}{runsetcmd} = 0;
    }

    ::readingsEndUpdate( $hash, 1 );

    Open($hash) if ( !::IsDisabled($name) && !$hash->{CD} );

    $hash->{helper}{device}{channelguide}{counter} =
      $hash->{helper}{device}{channelguide}{counter} + 1;
    ::InternalTimer( ::gettimeofday() + 10,
        \&FHEM::Devices::LGTV::LGTVWebOS::TimerStatusRequest, $hash );

    SocketKeepAlive($hash)
      if ( ::AttrVal( $name, 'keepAliveCheckTime', 0 ) > 0 )
      ;    # Check Socket KeepAlive

    return;
}

sub Set {    ## no critic (Subroutine "Set" with high complexity score)
    my ( $hash, $name, $cmd, @args ) = @_;
    my ( $arg, @params ) = @args;

    my $uri;
    my %payload;

    given ($cmd) {
        when ('connect') {
            return "usage: connect" if ( @args != 0 );

            Open($hash);

            return;

        }
        when ('clearInputList') {
            return "usage: clearInputList" if ( @args != 0 );

            delete $hash->{helper}{device}{inputs};
            delete $hash->{helper}{device}{inputapps};

            return;

        }
        when ('pairing') {
            return "usage: pairing" if ( @args != 0 );

            Pairing($hash);

            return;

        }
        when ('screenMsg') {
            return "usage: screenMsg <message>" if ( @args < 1 );

            my $msg = join( " ", @args );
            $payload{ $lgCommands{$cmd}->[1] } = decode_utf8($msg);
            $uri = $lgCommands{$cmd}->[0];

        }
        when ('off') {
            return "usage: on/off" if ( @args != 0 );

            $uri = $lgCommands{powerOff};
        }
        when ('on') {
            if ( ::AttrVal( $name, 'wakeOnLanMAC', 'none' ) ne 'none' ) {
                WakeUp_Udp(
                    $hash,
                    ::AttrVal( $name, 'wakeOnLanMAC',       0 ),
                    ::AttrVal( $name, 'wakeOnLanBroadcast', '255.255.255.255' )
                );
                return;
            }
            elsif ( ::AttrVal( $name, 'wakeupCmd', 'none' ) ne 'none' ) {
                my $wakeupCmd = ::AttrVal( $name, 'wakeupCmd', 'none' );
                if ( $wakeupCmd =~ s/^[ \t]*\{|\}[ \t]*$//xg ) {
                    ::Log3( $name, 4,
"LGTV_WebOS executing wake-up command (Perl): $wakeupCmd"
                    );
                    eval { $wakeupCmd } or do {
                        ::Log3( $name, 2,
"LGTV_WebOS executing wake-up command (Perl): $wakeupCmd failed"
                        );
                        return;
                    };
                    return;
                }
                else {
                    ::Log3( $name, 4,
"LGTV_WebOS executing wake-up command (fhem): $wakeupCmd"
                    );
                    ::fhem $wakeupCmd;
                    return;
                }
            }
            else {
                $uri = $lgCommands{powerOn};
            }
        }
        when ('3D') {
            return "usage: 3D on/off" if ( @args != 1 );

            if ( $args[0] eq 'off' ) {
                $uri = $lgCommands{'3DOff'};
            }
            elsif ( $args[0] eq 'on' ) {
                $uri = $lgCommands{'3DOn'};
            }

        }
        when ('mute') {
            return "usage: mute" if ( @args != 1 );

            if ( $args[0] eq 'off' ) {

                $uri = $lgCommands{volumeDown}->[0];

            }
            elsif ( $args[0] eq 'on' ) {

                $payload{ $lgCommands{$cmd}->[1] } = 'true';
                $uri = $lgCommands{$cmd}->[0];
            }

        }
        when ('volume') {
            return "usage: volume" if ( @args != 1 );

            $payload{ $lgCommands{$cmd}->[1] } = int( join( " ", @args ) );
            $uri = $lgCommands{$cmd}->[0];

        }
        when ('launchApp') {
            return "usage: launchApp" if ( @args != 1 );

            $payload{ $lgCommands{$cmd}->[1] } =
              $openApps{ join( " ", @args ) };
            $uri = $lgCommands{$cmd}->[0];

        }
        when ('input') {
            return "usage: input" if ( @args != 1 );

            my $inputLabel = join( " ", @args );
            $payload{ $lgCommands{launchApp}->[1] } =
              $hash->{helper}{device}{inputs}{$inputLabel};
            $uri = $lgCommands{launchApp}->[0];

        }
        when ('volumeUp') {
            return "usage: volumeUp" if ( @args != 0 );

            $uri = $lgCommands{$cmd}->[0];

        }
        when ('volumeDown') {
            return "usage: volumeDown" if ( @args != 0 );

            $uri = $lgCommands{$cmd}->[0];

        }
        when ('channelDown') {
            return "usage: channelDown" if ( @args != 0 );

            $uri = $lgCommands{$cmd}->[0];

        }
        when ('channelUp') {
            return "usage: channelUp" if ( @args != 0 );

            $uri = $lgCommands{$cmd}->[0];

        }
        when ('channel') {
            return "usage: channel" if ( @args != 1 );

            $payload{ $lgCommands{openChannel}->[1] } = join( " ", @args );
            $uri = $lgCommands{openChannel}->[0];

        }
        when ('getServiceList') {
            return "usage: getServiceList" if ( @args != 0 );

            $uri = $lgCommands{$cmd}->[0];

        }
        when ('getChannelList') {
            return "usage: getChannelList" if ( @args != 0 );

            $uri = $lgCommands{$cmd}->[0];

        }
        when ('getAppList') {
            return "usage: getAppList" if ( @args != 0 );

            $uri = $lgCommands{$cmd}->[0];

        }
        when ('getExternalInputList') {
            return "usage: getExternalInputList" if ( @args != 0 );

            $uri = $lgCommands{$cmd}->[0];

        }
        when ('play') {
            return "usage: play" if ( @args != 0 );

            $uri = $lgCommands{$cmd}->[0];

        }
        when ('stop') {
            return "usage: stop" if ( @args != 0 );

            $uri = $lgCommands{$cmd}->[0];

        }
        when ('fastForward') {
            return "usage: fastForward" if ( @args != 0 );

            $uri = $lgCommands{$cmd}->[0];

        }
        when ('rewind') {
            return "usage: rewind" if ( @args != 0 );

            $uri = $lgCommands{$cmd}->[0];

        }
        when ('pause') {
            return "usage: pause" if ( @args != 0 );

            $uri = $lgCommands{$cmd}->[0];

        }
        default {
            my $list = "";
            $list .=
'connect:noArg pairing:noArg screenMsg mute:on,off volume:slider,0,1,100 volumeUp:noArg volumeDown:noArg channelDown:noArg channelUp:noArg getServiceList:noArg on:noArg off:noArg';
            $list .=
' 3D:on,off stop:noArg play:noArg pause:noArg rewind:noArg fastForward:noArg clearInputList:noArg channel';
            $list .=
              ## no critic (Expression form of map. See page 169 of PBP)
              ' launchApp:' . join( ',', => map qq{$_} => keys %openApps );
            $list .= ' input:' . join(
                ',',
                ## no critic (Expression form of map. See page 169 of PBP)
                => map qq{$_} => keys %{ $hash->{helper}{device}{inputs} }
              )
              if ( exists( $hash->{helper}{device}{inputs} )
                && ref( $hash->{helper}{device}{inputs} ) eq "HASH" );

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

    $hash->{helper}{device}{runsetcmd} = $hash->{helper}{device}{runsetcmd} + 1;
    return CreateSendCommand( $hash, $uri, \%payload );
}

sub Open {
    my $hash    = shift;
    my $name    = $hash->{NAME};
    my $host    = $hash->{HOST};
    my $port    = 3000;
    my $timeout = 0.1;

    ::Log3( $name, 4, "LGTV_WebOS ($name) - Baue Socket Verbindung auf" );

 #  create a connecting socket
 #  SSL_startHandshake is dependent on the protocol: this lets us use one socket
 #  to work with either SSL or non-SSL sockets.
    my $socket = IO::Socket::SSL->new(
        PeerHost           => $host,
        PeerPort           => 3001,
        Proto              => 'tcp',
        SSL_startHandshake => 1,                 #( $proto eq 'wss' ? 1 : 0 ),
        SSL_verify_mode    => SSL_VERIFY_NONE,

        # Blocking                   => 1
        KeepAlive => 1,
        Timeout   => $timeout
      )
      || IO::Socket::INET->new(
        PeerHost  => $host,
        PeerPort  => $port,
        Proto     => 'tcp',
        KeepAlive => 1,
        Timeout   => $timeout
      )
      or return ::Log3( $name, 4,
        "LGTV_WebOS ($name) Couldn't connect to $host:$port/3001" )
      ;    # open Socket

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

    $hash->{helper}->{lastResponse} =
      int( ::gettimeofday() );         # Check Socket KeepAlive

    ::Log3( $name, 4, "LGTV_WebOS ($name) - Socket Connected" );

    Handshake($hash);
    ::Log3( $name, 4, "LGTV_WebOS ($name) - start Handshake" );

    return;
}

sub Close {
    my $hash = shift;
    my $name = $hash->{NAME};

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

    delete( $hash->{PARTIAL} );

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

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

    ::readingsSingleUpdate( $hash, 'state', 'off', 1 );
    ::readingsSingleUpdate( $hash, 'power', 'off', 1 );

    ::Log3( $name, 4, "LGTV_WebOS ($name) - Socket Disconnected" );

    return;
}

sub Write {
    my $hash   = shift;
    my $string = shift;

    my $name = $hash->{NAME};

    ::Log3( $name, 4, "LGTV_WebOS ($name) - WriteFn called" );

    return ::Log3( $name, 4, "LGTV_WebOS ($name) - socket not connected" )
      unless ( $hash->{CD} );

    ::Log3( $name, 4, "LGTV_WebOS ($name) - $string" );

    try {
        syswrite( $hash->{CD}, $string );
    }
    catch {
        if ( $_->isa('autodie::exception') && $_->matches(':io') ) {
            ::Log3( $name, 4,
"LGTV_WebOS ($name) - can't write to socket, autodie exception: $_"
            );

            Close($hash);
            return;
        }
        else {
            ::Log3( $name, 2,
                "LGTV_WebOS ($name) - can't write to socket: $_" );
            return;
        }
    };

    return;
}

sub SocketKeepAlive {
    my $hash = shift;
    my $name = $hash->{NAME};

    if (
        int( ::gettimeofday() ) - int( $hash->{helper}->{lastResponse} ) >
        ::AttrVal( $name, 'keepAliveCheckTime', 0 ) )
    {
        SocketClosePresenceAbsent( $hash, 'absent' );
        ::Log3( $name, 4,
"LGTV_WebOS ($name) - KeepAlive It looks like there no Data more response"
        );
    }

    return;
}

sub Read {
    my $hash = shift;
    my $name = $hash->{NAME};

    my $len;
    my $buf;

    ::Log3( $name, 4, "LGTV_WebOS ($name) - ReadFn started" );

    $hash->{helper}->{lastResponse} =
      int( ::gettimeofday() );    # Check Socket KeepAlive

    try {
        $len = sysread( $hash->{CD}, $buf, 10240 );
    }
    catch {
        if ( $_->isa('autodie::exception') && $_->matches(':io') ) {
            ::Log3( $name, 4,
"LGTV_WebOS ($name) - can't read from socket, autodie exception: $_"
            );

            Close($hash);
            return;
        }
        else {
            ::Log3( $name, 2,
                "LGTV_WebOS ($name) - can't read from socket: $_" );
            return;
        }
    };

    if ( !defined($len) || !$len ) {

        Close($hash);

        return;
    }

    unless ( defined $buf ) {
        ::Log3( $name, 3, "LGTV_WebOS ($name) - no data received" );
        return;
    }

    if ( $buf =~ /(\{"type":".+}}$)/x ) {

        $buf =~ /(\{"type":".+}}$)/x;
        ## no critic (Capture variable used outside conditional. See page 253 of PBP)
        $buf = $1;
        ## use critic

        ::Log3( $name, 4,
"LGTV_WebOS ($name) - received correct JSON string, start response processing: $buf"
        );

        ResponseProcessing( $hash, $buf );

    }
    elsif ( $buf =~ /HTTP\/1.1 101 Switching Protocols/x ) {

        ::Log3( $name, 4,
"LGTV_WebOS ($name) - received HTTP data string, start response processing: $buf"
        );

        ResponseProcessing( $hash, $buf );

    }
    else {

        ::Log3( $name, 4,
"LGTV_WebOS ($name) - coruppted data found, run LGTV_WebOS_ProcessRead: $buf"
        );

        ProcessRead( $hash, $buf );
    }

    return;
}

sub ProcessRead {
    my $hash = shift;
    my $data = shift;

    my $name = $hash->{NAME};

    my $buffer = '';

    ::Log3( $name, 4, "LGTV_WebOS ($name) - process read" );

    if ( exists( $hash->{PARTIAL} ) && $hash->{PARTIAL} ) {

        ::Log3( $name, 5, "LGTV_WebOS ($name) - PARTIAL: " . $hash->{PARTIAL} );
        $buffer = $hash->{PARTIAL};

    }
    else {

        ::Log3( $name, 4, "LGTV_WebOS ($name) - No PARTIAL buffer" );
    }

    ::Log3( $name, 5, "LGTV_WebOS ($name) - Incoming data: " . $data );

    $buffer = $buffer . $data;
    ::Log3( $name, 5,
"LGTV_WebOS ($name) - Current processing buffer (PARTIAL + incoming data): "
          . $buffer );

    my ( $json, $tail ) = ParseMsg( $hash, $buffer );

    while ($json) {

        $hash->{LAST_RECV} = time();

        ::Log3( $name, 5,
                "LGTV_WebOS ($name) - Decoding JSON message. Length: "
              . length($json)
              . " Content: "
              . $json );
        ::Log3( $name, 5,
                "LGTV_WebOS ($name) - Vor Sub: Laenge JSON: "
              . length($json)
              . " Content: "
              . $json
              . " Tail: "
              . $tail );

        ResponseProcessing( $hash, $json )
          if ( defined($tail) && ($tail) );

        ( $json, $tail ) = ParseMsg( $hash, $tail );

        ::Log3( $name, 5,
                "LGTV_WebOS ($name) - Nach Sub: Laenge JSON: "
              . length($json)
              . " Content: "
              . $json
              . " Tail: "
              . $tail );
    }

    $tail = ''
      if ( length($tail) > 30000 );
    $hash->{PARTIAL} = $tail;
    ::Log3( $name, 4, "LGTV_WebOS ($name) - PARTIAL lenght: " . length($tail) );

    ::Log3( $name, 5, "LGTV_WebOS ($name) - Tail: " . $tail );
    ::Log3( $name, 5, "LGTV_WebOS ($name) - PARTIAL: " . $hash->{PARTIAL} );

    return;
}

sub Handshake {
    my $hash = shift;

    my $name  = $hash->{NAME};
    my $host  = $hash->{HOST};
    my $wsKey = ::encode_base64( ::gettimeofday() );

    my $wsHandshakeCmd = "";
    $wsHandshakeCmd .= "GET / HTTP/1.1\r\n";
    $wsHandshakeCmd .= "Host: $host\r\n";
    $wsHandshakeCmd .= "User-Agent: FHEM\r\n";
    $wsHandshakeCmd .= "Upgrade: websocket\r\n";
    $wsHandshakeCmd .= "Connection: Upgrade\r\n";
    $wsHandshakeCmd .= "Sec-WebSocket-Version: 13\r\n";
    $wsHandshakeCmd .= "Sec-WebSocket-Key: " . $wsKey . "\r\n";

    Write( $hash, $wsHandshakeCmd );

    $hash->{helper}{wsKey} = $wsKey;

    ::Log3( $name, 4, "LGTV_WebOS ($name) - send Handshake to WriteFn" );

    TimerStatusRequest($hash);
    ::Log3( $name, 4, "LGTV_WebOS ($name) - start timer status request" );

    Pairing($hash);
    ::Log3( $name, 4, "LGTV_WebOS ($name) - start pairing routine" );

    return;
}

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

    ########################
    ### Response has HTML Header
    if ( $response =~ /HTTP\/1.1 101 Switching Protocols/x ) {

        my $data   = $response;
        my $header = Header2Hash($data);

        ################################
        ### Handshake for first Connect
        if ( exists( $header->{'Sec-WebSocket-Accept'} ) ) {

            my $keyAccept = $header->{'Sec-WebSocket-Accept'};
            ::Log3( $name, 5, "LGTV_WebOS ($name) - keyAccept: $keyAccept" );

            my $wsKey            = $hash->{helper}{wsKey};
            my $expectedResponse = trim(
                encode_base64(
                    pack(
                        'H*',
                        sha1_hex(
                            trim($wsKey)
                              . "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
                        )
                    )
                )
            );

            if ( $keyAccept eq $expectedResponse ) {

                ::Log3( $name, 3,
"LGTV_WebOS ($name) - Sucessfull WS connection to $hash->{HOST}"
                );
                ::readingsSingleUpdate( $hash, 'state', 'on', 1 );
                ::readingsSingleUpdate( $hash, 'power', 'on', 1 );

            }
            else {
                Close($hash);

                ::Log3( $name, 3,
"LGTV_WebOS ($name) - ERROR: Unsucessfull WS connection to $hash->{HOST}"
                );
            }
        }

        return;
    }

    elsif ( $response =~ m/^{"type":".+}}$/x ) {

        return ::Log3( $name, 4,
            "LGTV_WebOS ($name) - garbage after JSON object" )
          if ( $response =~ m/^{"type":".+}}.+{"type":".+/x );

        ::Log3( $name, 4,
            "LGTV_WebOS ($name) - JSON detected, run LGTV_WebOS_WriteReadings"
        );

        my $json = $response;

        ::Log3( $name, 4, "LGTV_WebOS ($name) - Corrected JSON String: $json" )
          if ($json);

        if ( !defined($json) || !($json) ) {

            ::Log3( $name, 4,
                "LGTV_WebOS ($name) - Corrected JSON String empty" );
            return;
        }

        my $decode_json;
        try {
            $decode_json = decode_json( encode_utf8($json) );
        }
        catch {
            if ( $_->isa('autodie::exception') && $_->matches(':io') ) {
                Log3( $name, 3,
                    "LGTV_WebOS ($name) autodie - JSON error while request: $_"
                );
                return;
            }
            else {
                Log3( $name, 3,
                    "LGTV_WebOS ($name) - JSON error while request: $_" );
                return;
            }
        };    # Note semicolon.

        WriteReadings( $hash, $decode_json );

        return;
    }

    ::Log3( $name, 4, "LGTV_WebOS ($name) - no Match found" );

    return;
}

sub WriteServiceReadings {
    my $hash        = shift;
    my $decode_json = shift;

    for my $services ( @{ $decode_json->{payload}{services} } ) {
        ::readingsBulkUpdateIfChanged(
            $hash,
            'service_' . $services->{name},
            'v.' . $services->{version}
        );
    }

    return;
}

sub WriteDeviceReadings {
    my $hash        = shift;
    my $decode_json = shift;

    for my $devices ( @{ $decode_json->{payload}{devices} } ) {

        if (   !exists( $hash->{helper}{device}{inputs}{ $devices->{label} } )
            || !
            exists( $hash->{helper}{device}{inputapps}{ $devices->{appId} } ) )
        {

            $hash->{helper}{device}{inputs}
              { ::makeDeviceName( $devices->{label} ) } = $devices->{appId};
            $hash->{helper}{device}{inputapps}{ $devices->{appId} } =
              ::makeDeviceName( $devices->{label} );
        }

        ::readingsBulkUpdateIfChanged(
            $hash,
            'extInput_' . ::makeDeviceName( $devices->{label} ),
            'connect_' . $devices->{connected}
        );
    }

    return;
}

sub WriteProgramlistReadings {
    my $hash        = shift;
    my $decode_json = shift;

    require Date::Parse;
    my $count = 0;
    for my $programList ( @{ $decode_json->{payload}{programList} } ) {

        if (
            ::str2time( FormartStartEndTime( $programList->{localEndTime} ) ) >
            time() )
        {
            if ( $count < 1 ) {

                ::readingsBulkUpdateIfChanged( $hash, 'channelCurrentTitle',
                    $programList->{programName} );
                ::readingsBulkUpdateIfChanged( $hash, 'channelCurrentStartTime',
                    FormartStartEndTime( $programList->{localStartTime} ) );
                ::readingsBulkUpdateIfChanged( $hash, 'channelCurrentEndTime',
                    FormartStartEndTime( $programList->{localEndTime} ) );

            }
            elsif ( $count < 2 ) {

                ::readingsBulkUpdateIfChanged( $hash, 'channelNextTitle',
                    $programList->{programName} );
                ::readingsBulkUpdateIfChanged( $hash, 'channelNextStartTime',
                    FormartStartEndTime( $programList->{localStartTime} ) );
                ::readingsBulkUpdateIfChanged( $hash, 'channelNextEndTime',
                    FormartStartEndTime( $programList->{localEndTime} ) );
            }

            $count++;
            return if ( $count > 1 );
        }
    }

    return;
}

sub WriteMuteReadings {
    my $hash        = shift;
    my $decode_json = shift;

    if (
        exists( $decode_json->{payload}{'mute'} )
        && (   $decode_json->{payload}{'mute'} eq 'true'
            || $decode_json->{payload}{'mute'} == 1 )
      )
    {

        ::readingsBulkUpdateIfChanged( $hash, 'mute', 'on' );

    }
    elsif ( exists( $decode_json->{payload}{'mute'} ) ) {
        if (   $decode_json->{payload}{'mute'} eq 'false'
            || $decode_json->{payload}{'mute'} == 0 )
        {

            ::readingsBulkUpdateIfChanged( $hash, 'mute', 'off' );
        }
    }

    if (
        exists( $decode_json->{payload}{'muted'} )
        && (   $decode_json->{payload}{'muted'} eq 'true'
            || $decode_json->{payload}{'muted'} == 1 )
      )
    {

        ::readingsBulkUpdateIfChanged( $hash, 'mute', 'on' );

    }
    elsif (
        exists( $decode_json->{payload}{'muted'} )
        && (   $decode_json->{payload}{'muted'} eq 'false'
            || $decode_json->{payload}{'muted'} == 0 )
      )
    {

        ::readingsBulkUpdateIfChanged( $hash, 'mute', 'off' );
    }

    return;
}

sub Write3dReadings {
    my $hash        = shift;
    my $decode_json = shift;

    if (   $decode_json->{payload}{status3D}{status} eq 'false'
        || $decode_json->{payload}{status3D}{status} == 0 )
    {

        ::readingsBulkUpdateIfChanged( $hash, '3D', 'off' );

    }
    elsif ($decode_json->{payload}{status3D}{status} eq 'true'
        || $decode_json->{payload}{status3D}{status} == 1 )
    {

        ::readingsBulkUpdateIfChanged( $hash, '3D', 'on' );
    }

    ::readingsBulkUpdateIfChanged( $hash, '3DMode',
        $decode_json->{payload}{status3D}{pattern} );

    return;
}

sub WriteAppIdReadings {
    my $hash        = shift;
    my $decode_json = shift;

    if (
        (
               $decode_json->{payload}{appId} =~ /com.webos.app.externalinput/x
            || $decode_json->{payload}{appId} =~ /com.webos.app.hdmi/x
        )
        && exists(
            $hash->{helper}{device}{inputapps}{ $decode_json->{payload}{appId} }
        )
        && $hash->{helper}{device}{inputapps}{ $decode_json->{payload}{appId} }
      )
    {

        ::readingsBulkUpdateIfChanged( $hash, 'input',
            $hash->{helper}{device}{inputapps}{ $decode_json->{payload}{appId} }
        );
        ::readingsBulkUpdateIfChanged( $hash, 'launchApp', '-' );

    }
    elsif ( exists( $openAppsPackageName{ $decode_json->{payload}{appId} } )
        && $openAppsPackageName{ $decode_json->{payload}{appId} } )
    {

        ::readingsBulkUpdateIfChanged( $hash, 'launchApp',
            $openAppsPackageName{ $decode_json->{payload}{appId} } );
        ::readingsBulkUpdateIfChanged( $hash, 'input', '-' );
    }

    return;
}

sub WriteTypeReadings {
    my $hash        = shift;
    my $decode_json = shift;

    my $response;

    if ( $decode_json->{type} eq 'registered'
        && exists( $decode_json->{payload}{'client-key'} ) )
    {

        $hash->{helper}{device}{registered} = 1;

    }
    elsif (
        (
            $decode_json->{type} eq 'response'
            && (   $decode_json->{payload}{returnValue} eq 'true'
                || $decode_json->{payload}{returnValue} == 1 )
        )
        || ( $decode_json->{type} eq 'registered' )
        && exists( $decode_json->{payload}{'client-key'} )
      )
    {

        $response = 'ok';
        ::readingsBulkUpdateIfChanged( $hash, 'pairing', 'paired' );
        $hash->{helper}{device}{runsetcmd} =
          $hash->{helper}{device}{runsetcmd} - 1
          if ( $hash->{helper}{device}{runsetcmd} > 0 );

    }
    elsif ( $decode_json->{type} eq 'error' ) {

        $response = "error - $decode_json->{error}"
          if ( $decode_json->{error} ne '404 no such service or method' );

        if (   $decode_json->{error} eq '401 insufficient permissions'
            || $decode_json->{error} eq
            '401 insufficient permissions (not registered)' )
        {

            ::readingsBulkUpdateIfChanged( $hash, 'pairing', 'unpaired' );
        }

        $hash->{helper}{device}{runsetcmd} =
          $hash->{helper}{device}{runsetcmd} - 1
          if ( $hash->{helper}{device}{runsetcmd} > 0 );
    }

    return $response;
}

sub WriteReadings {
    my ( $hash, $decode_json ) = @_;

    my $name = $hash->{NAME};
    my $response;

    ::Log3( $name, 4, "LGTV_WebOS ($name) - Beginn Readings writing" );

    ::readingsBeginUpdate($hash);

    if ( ref( $decode_json->{payload}{services} ) eq "ARRAY"
        && scalar( @{ $decode_json->{payload}{services} } ) > 0 )
    {
        WriteServiceReadings( $hash, $decode_json );
    }
    elsif ( ref( $decode_json->{payload}{devices} ) eq "ARRAY"
        && scalar( @{ $decode_json->{payload}{devices} } ) > 0 )
    {
        WriteDeviceReadings( $hash, $decode_json );
    }
    elsif ( ref( $decode_json->{payload}{programList} ) eq "ARRAY"
        && scalar( @{ $decode_json->{payload}{programList} } ) > 0 )
    {
        WriteProgramlistReadings( $hash, $decode_json );
    }

    if (   exists( $decode_json->{payload}{'mute'} )
        || exists( $decode_json->{payload}{'muted'} ) )
    {
        WriteMuteReadings( $hash, $decode_json );
    }
    elsif ( exists( $decode_json->{payload}{status3D}{status} ) ) {
        Write3dReadings( $hash, $decode_json );
    }
    elsif ( exists( $decode_json->{payload}{appId} ) ) {
        WriteAppIdReadings( $hash, $decode_json );
    }

    if ( exists( $decode_json->{type} ) ) {
        $response = WriteTypeReadings( $hash, $decode_json );
    }

    ::readingsBulkUpdateIfChanged( $hash, 'lgKey',
        $decode_json->{payload}{'client-key'} )
      if ( exists( $decode_json->{payload}{'client-key'} ) );
    ::readingsBulkUpdateIfChanged( $hash, 'volume',
        $decode_json->{payload}{'volume'} )
      if ( exists( $decode_json->{payload}{'volume'} ) );
    ::readingsBulkUpdateIfChanged( $hash, 'lastResponse', $response )
      if ( defined($response) );

    if ( ::ReadingsVal( $name, 'launchApp', 'none' ) eq 'TV' ) {

        ::readingsBulkUpdateIfChanged( $hash, 'channel',
            $decode_json->{payload}{'channelNumber'} )
          if ( exists( $decode_json->{payload}{'channelNumber'} ) );
        ::readingsBulkUpdateIfChanged( $hash, 'channelName',
            $decode_json->{payload}{'channelName'} )
          if ( exists( $decode_json->{payload}{'channelName'} ) );
        ::readingsBulkUpdateIfChanged( $hash, 'channelMedia',
            $decode_json->{payload}{'channelTypeName'} )
          if ( exists( $decode_json->{payload}{'channelTypeName'} ) );

    }
    else {

        ::readingsBulkUpdateIfChanged( $hash, 'channelName',             '-' );
        ::readingsBulkUpdateIfChanged( $hash, 'channel',                 '-' );
        ::readingsBulkUpdateIfChanged( $hash, 'channelMedia',            '-' );
        ::readingsBulkUpdateIfChanged( $hash, 'channelCurrentTitle',     '-' );
        ::readingsBulkUpdateIfChanged( $hash, 'channelCurrentStartTime', '-' );
        ::readingsBulkUpdateIfChanged( $hash, 'channelCurrentEndTime',   '-' );
        ::readingsBulkUpdateIfChanged( $hash, 'channelNextTitle',        '-' );
        ::readingsBulkUpdateIfChanged( $hash, 'channelNextStartTime',    '-' );
        ::readingsBulkUpdateIfChanged( $hash, 'channelNextEndTime',      '-' );
    }

    ::readingsBulkUpdateIfChanged( $hash, 'state', 'on' );
    ::readingsBulkUpdateIfChanged( $hash, 'power', 'on' );

    ::readingsEndUpdate( $hash, 1 );

    return;
}

sub Pairing {
    my $hash = shift;
    my $name = $hash->{NAME};

    ::Log3( $name, 4, "LGTV_WebOS ($name) - HASH handshakePayload" );

    my %handshakePayload = (
        "pairingType" => "PROMPT",
        "manifest"    => {
            "manifestVersion" => 1,
            "appVersion"      => "1.1",
            "signed"          => {
                "created"           => "20161123",
                "appId"             => "com.lge.test",
                "vendorId"          => "com.lge",
                "localizedAppNames" => {
                    ""      => "FHEM LG Remote",
                    "de-DE" => "FHEM LG Fernbedienung"
                },
                "localizedVendorNames" => {
                    "" => "LG Electronics"
                },
                "permissions" => [
                    "TEST_SECURE",                "CONTROL_INPUT_TEXT",
                    "CONTROL_MOUSE_AND_KEYBOARD", "READ_INSTALLED_APPS",
                    "READ_LGE_SDX",               "READ_NOTIFICATIONS",
                    "SEARCH",                     "WRITE_SETTINGS",
                    "WRITE_NOTIFICATION_ALERT",   "CONTROL_POWER",
                    "READ_CURRENT_CHANNEL",       "READ_RUNNING_APPS",
                    "READ_UPDATE_INFO",           "UPDATE_FROM_REMOTE_APP",
                    "READ_LGE_TV_INPUT_EVENTS",   "READ_TV_CURRENT_TIME"
                ],
                "serial" => "2f930e2d2cfe083771f68e4fe7bb07"
            },
            "permissions" => [
                "LAUNCH",
                "LAUNCH_WEBAPP",
                "APP_TO_APP",
                "CLOSE",
                "TEST_OPEN",
                "TEST_PROTECTED",
                "CONTROL_AUDIO",
                "CONTROL_DISPLAY",
                "CONTROL_INPUT_JOYSTICK",
                "CONTROL_INPUT_MEDIA_RECORDING",
                "CONTROL_INPUT_MEDIA_PLAYBACK",
                "CONTROL_INPUT_TV",
                "CONTROL_POWER",
                "READ_APP_STATUS",
                "READ_CURRENT_CHANNEL",
                "READ_INPUT_DEVICE_LIST",
                "READ_NETWORK_STATE",
                "READ_RUNNING_APPS",
                "READ_TV_CHANNEL_LIST",
                "WRITE_NOTIFICATION_TOAST",
                "READ_POWER_STATE",
                "READ_COUNTRY_INFO"
            ],
            "signatures" => [
                {
                    "signatureVersion" => 1,
                    "signature"        =>
"eyJhbGdvcml0aG0iOiJSU0EtU0hBMjU2Iiwia2V5SWQiOiJ0ZXN0LXNpZ25pbmctY2VydCIsInNpZ25hdHVyZVZlcnNpb24iOjF9.hrVRgjCwXVvE2OOSpDZ58hR+59aFNwYDyjQgKk3auukd7pcegmE2CzPCa0bJ0ZsRAcKkCTJrWo5iDzNhMBWRyaMOv5zWSrthlf7G128qvIlpMT0YNY+n/FaOHE73uLrS/g7swl3/qH/BGFG2Hu4RlL48eb3lLKqTt2xKHdCs6Cd4RMfJPYnzgvI4BNrFUKsjkcu+WD4OO2A27Pq1n50cMchmcaXadJhGrOqH5YmHdOCj5NSHzJYrsW0HPlpuAx/ECMeIZYDh6RMqaFM2DXzdKX9NmmyqzJ3o/0lkk/N97gfVRLW5hA29yeAwaCViZNCP8iC9aO0q9fQojoa7NQnAtw=="
                }
            ]
        }
    );

    my $usedHandshake = \%handshakePayload;

    my $key = ::ReadingsVal( $name, 'lgKey', '' );

    $usedHandshake->{'client-key'} = $key if ( defined($key) );

    CreateSendCommand( $hash, undef, $usedHandshake, 'register' );
    ::Log3( $name, 4, "LGTV_WebOS ($name) - Send pairing informations" );

    return;
}

sub CreateSendCommand {
    my ( $hash, $uri, $payload, $type ) = @_;

    my $name = $hash->{NAME};

    $type = 'request' if ( !defined($type) );

    my $command = {};
    $command->{'client-key'} = ::ReadingsVal( $name, 'lgKey', '' )
      if ( $type ne 'register' );
    $command->{id}      = $type . "_" . ::gettimeofday();
    $command->{type}    = $type;
    $command->{uri}     = $uri     if ($uri);
    $command->{payload} = $payload if ( defined($payload) );

#::Log3( $name, 5, "LGTV_WebOS ($name) - Payload Message: $command->{payload}{message}" );

    my $cmd;
    try {
        $cmd = encode_json($command);
    }
    catch {
        if ( $_->isa('autodie::exception') && $_->matches(':io') ) {
            Log3( $name, 3,
                "LGTV_WebOS ($name) - can't $cmd encode to json: $_" );
            return;
        }
        else {
            Log3( $name, 3,
                "LGTV_WebOS ($name) - can't $cmd encode to json: $_" );
            return;
        }
    };

    ::Log3( $name, 5, "LGTV_WebOS ($name) - Sending command: $cmd" );

    Write( $hash, Hybi10Encode( $cmd, "text", 1 ) );

    return;
}

sub Hybi10Encode {
    my $payload = shift;
    my $type    = shift // 'text';
    my $masked  = shift // 1;

    my @frameHead;
    my $frame         = "";
    my $payloadLength = length($payload);

    given ($type) {
        when ('text') {

            # first byte indicates FIN, Text-Frame (10000001):
            $frameHead[0] = 129;
        }
        when ('close') {

            # first byte indicates FIN, Close Frame(10001000):
            $frameHead[0] = 136;
        }
        when ('ping') {

            # first byte indicates FIN, Ping frame (10001001):
            $frameHead[0] = 137;
        }
        when ('pong') {

            # first byte indicates FIN, Pong frame (10001010):
            $frameHead[0] = 138;
        }
    }

    # set mask and payload length (using 1, 3 or 9 bytes)
    if ( $payloadLength > 65535 ) {

        # TODO
        my $payloadLengthBin = sprintf( '%064b', $payloadLength );
        $frameHead[1] = ($masked) ? 255 : 127;

        for ( my $i = 0 ; $i < 8 ; $i++ ) {

            $frameHead[ $i + 2 ] =
              oct( "0b" . substr( $payloadLengthBin, $i * 8, $i * 8 + 8 ) );
        }

        # most significant bit MUST be 0 (close connection if frame too big)
        if ( $frameHead[2] > 127 ) {

            #$this->close(1004);
            return;
        }

    }
    elsif ( $payloadLength > 125 ) {

        my $payloadLengthBin = sprintf( '%016b', $payloadLength );
        $frameHead[1] = ($masked) ? 254 : 126;
        $frameHead[2] = oct( "0b" . substr( $payloadLengthBin, 0, 8 ) );
        $frameHead[3] = oct( "0b" . substr( $payloadLengthBin, 8, 16 ) );

    }
    else {

        $frameHead[1] = ($masked) ? $payloadLength + 128 : $payloadLength;
    }

    # convert frame-head to string:
    for ( my $i = 0 ; $i < scalar(@frameHead) ; $i++ ) {

        $frameHead[$i] = chr( $frameHead[$i] );
    }

    my @mask;
    if ($masked) {

        # generate a random mask:
        for ( my $i = 0 ; $i < 4 ; $i++ ) {

            #$mask[$i] = chr(int(rand(255)));
            $mask[$i] = chr( int( 25 * $i ) );
        }

        @frameHead = ( @frameHead, @mask );
    }

    $frame = join( "", @frameHead );

    # append payload to frame:
    my $char;
    for ( my $i = 0 ; $i < $payloadLength ; $i++ ) {

        $char = substr( $payload, $i, 1 );
        $frame .= ($masked) ? $char ^ $mask[ $i % 4 ] : $char;
    }

    return $frame;
}

sub GetAudioStatus {
    my $hash = shift;
    my $name = $hash->{NAME};

    ::Log3( $name, 4,
        "LGTV_WebOS ($name) - LGTV_WebOS_GetAudioStatus: "
          . $hash->{helper}{device}{runsetcmd} );
    CreateSendCommand( $hash, $lgCommands{getAudioStatus}, undef )
      if ( $hash->{helper}{device}{runsetcmd} == 0 );

    return;
}

sub GetCurrentChannel {
    my $hash = shift;
    my $name = $hash->{NAME};

    ::RemoveInternalTimer( $hash,
        \&FHEM::Devices::LGTV::LGTVWebOS::GetCurrentChannel );
    ::Log3( $name, 4,
        "LGTV_WebOS ($name) - LGTV_WebOS_GetCurrentChannel: "
          . $hash->{helper}{device}{runsetcmd} );
    CreateSendCommand( $hash, $lgCommands{getCurrentChannel}, undef )
      if ( $hash->{helper}{device}{runsetcmd} == 0 );

    return;
}

sub GetForgroundAppInfo {
    my $hash = shift;
    my $name = $hash->{NAME};

    ::RemoveInternalTimer( $hash,
        \&FHEM::Devices::LGTV::LGTVWebOS::GetForgroundAppInfo );
    ::Log3( $name, 4,
        "LGTV_WebOS ($name) - LGTV_WebOS_GetForgroundAppInfo: "
          . $hash->{helper}{device}{runsetcmd} );
    CreateSendCommand( $hash, $lgCommands{getForegroundAppInfo}, undef )
      if ( $hash->{helper}{device}{runsetcmd} == 0 );

    return;
}

sub GetExternalInputList {
    my $hash = shift;
    my $name = $hash->{NAME};

    ::RemoveInternalTimer( $hash,
        \&FHEM::Devices::LGTV::LGTVWebOS::GetExternalInputList );
    ::Log3( $name, 4,
        "LGTV_WebOS ($name) - LGTV_WebOS_GetExternalInputList: "
          . $hash->{helper}{device}{runsetcmd} );
    CreateSendCommand( $hash, $lgCommands{getExternalInputList}, undef )
      if ( $hash->{helper}{device}{runsetcmd} == 0 );

    return;
}

sub Get3DStatus {
    my $hash = shift;
    my $name = $hash->{NAME};

    ::RemoveInternalTimer( $hash,
        \&FHEM::Devices::LGTV::LGTVWebOS::Get3DStatus );
    ::Log3( $name, 4,
        "LGTV_WebOS ($name) - LGTV_WebOS_Get3DStatus: "
          . $hash->{helper}{device}{runsetcmd} );
    CreateSendCommand( $hash, $lgCommands{get3DStatus}, undef )
      if ( $hash->{helper}{device}{runsetcmd} == 0 );

    return;
}

sub GetChannelProgramInfo {
    my $hash = shift;
    my $name = $hash->{NAME};

    ::Log3( $name, 4,
        "LGTV_WebOS ($name) - LGTV_WebOS_GetChannelProgramInfo: "
          . $hash->{helper}{device}{runsetcmd} );
    CreateSendCommand( $hash, $lgCommands{getChannelProgramInfo}, undef )
      if ( $hash->{helper}{device}{runsetcmd} == 0 );

    return;
}

#############################################
### my little Helper

sub ParseMsg {
    my $hash   = shift;
    my $buffer = shift;

    my $name      = $hash->{NAME};
    my $jsonopen  = 0;
    my $jsonclose = 0;
    my $msg       = '';
    my $tail      = '';

    if ($buffer) {
        for my $c ( split //, $buffer ) {
            if ( $jsonopen == $jsonclose && $jsonopen > 0 ) {
                $tail .= $c;
                ::Log3( $name, 5,
"LGTV_WebOS ($name) - $jsonopen == $jsonclose && $jsonopen > 0"
                );

            }
            elsif ( ( $jsonopen == $jsonclose ) && ( $c ne '{' ) ) {

                ::Log3( $name, 5,
                    "LGTV_WebOS ($name) - Garbage character before message: "
                      . $c );

            }
            else {

                if ( $c eq '{' ) {

                    $jsonopen++;

                }
                elsif ( $c eq '}' ) {

                    $jsonclose++;
                }

                $msg .= $c;
            }
        }

        if ( $jsonopen != $jsonclose ) {

            $tail = $msg;
            $msg  = '';
        }
    }

    ::Log3( $name, 5, "LGTV_WebOS ($name) - return msg: $msg and tail: $tail" );
    return ( $msg, $tail );
}

sub Header2Hash {
    my $string = shift;
    my %hash   = ();

    for my $line ( split( "\r\n", $string ) ) {
        my ( $key, $value ) = split( ": ", $line );
        next if ( !$value );

        $value =~ s/^ //x;
        $hash{$key} = $value;
    }

    return \%hash;
}

sub FormartStartEndTime {
    my $string = shift;

    my @timeArray = split( ',', $string );

    return
"$timeArray[0]-$timeArray[1]-$timeArray[2] $timeArray[3]:$timeArray[4]:$timeArray[5]";
}

############ Presence Erkennung Begin #################
sub Presence {
    my $hash = shift;
    my $name = $hash->{NAME};

    $hash->{helper}{RUNNING_PID} = ::BlockingCall(
        'FHEM::Devices::LGTV::LGTVWebOS::PresenceRun',
        $name . '|' . $hash->{HOST},
        'FHEM::Devices::LGTV::LGTVWebOS::PresenceDone',
        5,
        'FHEM::Devices::LGTV::LGTVWebOS::PresenceAborted',
        $hash
    ) unless ( exists( $hash->{helper}{RUNNING_PID} ) );

    return;
}

sub PresenceRun {
    my $string = shift;
    my ( $name, $host ) = split( "\\|", $string );

    my $tmp;
    my $response;

    $tmp = qx(ping -c 3 -w 2 $host 2>&1);  ## no critic (Backtick operator used)

    if ( defined($tmp) && $tmp ne '' ) {

        chomp $tmp;
        ::Log3( $name, 4,
            "LGTV_WebOS ($name) - ping command returned with output:\n$tmp" );
        $response = $name . '|' . (
            $tmp =~
              /\d+ [Bb]ytes (from|von)/ ## no critic (Regular expression without "/x")
              && $tmp !~ /[Uu]nreachable/x
            ? 'present'
            : 'absent'
        );

    }
    else {

        $response = "$name|Could not execute ping command";
    }

    ::Log3( $name, 4,
        "sub PresenceRun ($name) - Sub finish, Call LGTV_WebOS_PresenceDone" );

    return $response;
}

sub PresenceDone {
    my $string = shift;

    my ( $name, $response ) = split( "\\|", $string );
    my $hash = $defs{$name};

    delete( $hash->{helper}{RUNNING_PID} );

    if ( exists( $hash->{helper}{DISABLED} ) ) {
        ::Log3( $name, 4,
            "sub PresenceDone ($name) - Helper is disabled. Stop processing" );

        return;
    }

    ::readingsSingleUpdate( $hash, 'presence', $response, 1 );

    SocketClosePresenceAbsent( $hash, $response );

    ::Log3( $name, 4, "sub PresenceDone ($name) - presence done" );

    return;
}

sub PresenceAborted {
    my $hash = shift;
    my $name = $hash->{NAME};

    delete( $hash->{helper}{RUNNING_PID} );
    ::readingsSingleUpdate( $hash, 'presence', 'pingPresence timedout', 1 );

    ::Log3( $name, 4,
"sub PresenceAborted ($name) - The BlockingCall Process terminated unexpectedly. Timedout!"
    );

    return;
}

sub SocketClosePresenceAbsent {

    my $hash     = shift;
    my $presence = shift;

    my $name = $hash->{NAME};

    Close($hash)
      if ( $presence eq 'absent' && !::IsDisabled($name) && $hash->{CD} )
      ;   # https://forum.fhem.de/index.php/topic,66671.msg694578.html#msg694578
     # Sobald pingPresence absent meldet und der Socket noch steht soll er geschlossen werden, da sonst FHEM nach 4-6 min für 10 min blockiert

    return;
}

sub WakeUp_Udp {
    my ( $hash, $mac_addr, $host, $port ) = @_;
    my $name = $hash->{NAME};

    $port = 9 if ( !defined $port || $port !~ /^\d+$/x );

    my $sock = IO::Socket::INET->new( Proto => 'udp' ) or warn "socket : $!\n";
    if ( !$sock ) {
        ::Log3( $name, 3, "sub WakeUp_Udp ($name) - Can't create WOL socket" );
        return 1;
    }

    my $ip_addr   = ::inet_aton($host);
    my $sock_addr = ::sockaddr_in( $port, $ip_addr );
    $mac_addr =~ s/://xg;
    my $packet =
      pack( 'C6H*', 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, $mac_addr x 16 );

    setsockopt( $sock, ::SOL_SOCKET, ::SO_BROADCAST, 1 )
      or warn "setsockopt : $!\n";
    send( $sock, $packet, 0, $sock_addr ) or warn "send : $!\n";
    close($sock);

    return 1;
}

####### Presence Erkennung Ende ############

1;

__END__
