# $Id$ 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_Initialize($) { my ($hash) = @_; $hash->{ReadFn} = "harmony_Read"; $hash->{DefFn} = "harmony_Define"; $hash->{NOTIFYDEV} = "global"; $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 $readingFnAttributes"; $hash->{FW_detailFn} = "harmony_detailFn"; } ##################################### sub harmony_Define($$) { my ($hash, $def) = @_; my @a = split("[ \t][ \t]*", $def); return "Usage: define harmony [username password] ip" if(@a < 3 || @a > 5); return "Usage: define 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]; $hash->{ip} = $ip; } elsif( @a == 4 ) { my $id = $a[3]; return "$name: device '$id' already defined" if( defined($modules{$hash->{TYPE}}{defptr}{$id}) ); $hash->{id} = $id; $modules{$hash->{TYPE}}{defptr}{$id} = $hash; } elsif( @a == 5 ) { my $username = $a[2]; my $password = $a[3]; my $ip = $a[4]; $hash->{helper}{username} = $username; $hash->{helper}{password} = $password; $hash->{ip} = $ip; } $hash->{NAME} = $name; $hash->{STATE} = "Initialized"; $hash->{ConnectionState} = "Initialized"; #$attr{$name}{nossl} = 1 if( !$init_done && harmony_isFritzBox() ); if( $init_done ) { harmony_connect($hash) if( !defined($hash->{id}) ); } return undef; } sub harmony_Notify($$) { my ($hash,$dev) = @_; return if($dev->{NAME} ne "global"); return if(!grep(m/^INITIALIZED|REREADCFG$/, @{$dev->{CHANGED}})); harmony_connect($hash) if( !defined($hash->{id}) ); return undef; } sub harmony_Undefine($$) { my ($hash, $arg) = @_; if( defined($hash->{id}) ) { delete( $modules{$hash->{TYPE}}{defptr}{$hash->{id}} ); return undef; } 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 "myHarmony config
" } 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( !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 ) { return decode_json($function->{action}) if( harmony_isFritzBox() ); return JSON->new->utf8(0)->decode($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, $param2) = @params; #$cmd = lc( $cmd ); 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, "deviceId=$id"); 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"); return undef; } elsif( $cmd eq "channel" ) { 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 ); } Log3 $name, 4, "$name: sending $action->{command} 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, (0.1)); $payload = "status=release:action={'command'::'$action->{command}','type'::'$action->{type}','deviceId'::'$action->{deviceId}'}:timestamp=100"; 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, "deviceId=$id"); 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 eq "all" ); delete $hash->{identity} if( $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, "interval=$interval", "setsleeptimer"); return undef; } elsif( $cmd eq "sync" ) { harmony_sendIq($hash, ""); return undef; } elsif( $cmd eq "update" ) { harmony_sendIq($hash, "format=json"); return undef; } elsif( $cmd eq "xxx" ) { #harmony_sendIq($hash, "format=json"); #harmony_sendIq($hash, "format=json"); #harmony_sendIq($hash, "format=json"); #harmony_sendIq($hash, ""); #harmony_sendIq($hash, ""); #harmony_sendIq($hash, ""); #harmony_sendIq($hash, ""); #harmony_sendIq($hash, ""); #harmony_sendIq($hash, ""); 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 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( !$hash->{helper}{username} ) { $hash->{helper}{UserAuthToken} = ""; return; } my $https = "https"; $https = "http" if( AttrVal($hash->{NAME}, "nossl", 0) ); my $json = encode_json( { email => $hash->{helper}{username}, password => $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 {...:...} 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; readingsSingleUpdate( $hash, "activity", $activity, 1 ) if( !$modifier && $activity ne ReadingsVal($hash->{NAME},"activity", "" ) ); delete $hash->{hidDevice} if( $id == -1 ); } sub harmony_Read($) { my ($hash) = @_; my $name = $hash->{NAME}; 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; #FIXME: should use real xmpp/xml parser 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 "" ) { $hash->{ConnectionState} = "LoggedIn"; if( $hash->{helper}{UserAuthToken} ) { harmony_getSessionToken($hash); } else { #harmony_sendIq($hash, ""); #harmony_sendIq($hash, ""); #harmony_sendIq($hash, ""); #harmony_sendIq($hash, ""); #harmony_sendIq($hash, "format=json"); #harmony_sendIq($hash, "format=json"); #harmony_sendIq($hash, "format=json"); #harmony_sendIq($hash, "format=json"); #harmony_sendIq($hash, "hetag= :uri=dynamite:://HomeAutomationService/Config/:encode=true"); #harmony_sendIq($hash, ""); #harmony_sendIq($hash, "name=1vm7ATw/tN6HXGpQcCs/A5MkuvI#iOS6.0.1#iPhone"); harmony_sendIq($hash, "format=json"); #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// ) { my $cdata = $1; my $json; my $decoded; if( $cdata =~ m/^{.*}$/ ) { if( harmony_isFritzBox() ) { $json = decode_json($cdata); } else { $json = JSON->new->utf8(0)->decode($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, "format=json"); } $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}; } else { harmony_sendEngineGet($hash, "gettimerinterval", "timerId=$decoded->{sleepTimerId}"); } } } } elsif( $tag eq "message" ) { if( $content =~ m/type="harmony.engine\?startActivityFinished"/ ) { harmony_updateActivity($hash, $decoded->{activityId}) if( defined($decoded->{activityId}) ); } 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, "format=json"); #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 = "" 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} ); } 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, "hetag= :uri=content:://1.0/user;$station:encode=true"); #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, "hetag= :uri=content:://1.0/user;$content:encode=true"); # last; #} #harmony_sendIq($hash, "hetag= :uri=content:://1.0/user;$hash->{config}->{content}->{householdUserProfileUri}:encode=true"); harmony_sendIq($hash, "format=json"); #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 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>/ ) { 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"); $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_disconnect($) { my ($hash) = @_; my $name = $hash->{NAME}; RemoveInternalTimer($hash); $hash->{ConnectionState} = "Disconnected"; readingsSingleUpdate( $hash, "state", $hash->{ConnectionState}, 1 ) if( $hash->{ConnectionState} ne ReadingsVal($hash->{NAME},"state", "" ) ); 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( AttrVal($hash->{NAME}, "disable", 0) ); harmony_disconnect($hash); Log3 $name, 4, "$name: connect"; 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->{ConnectionState} = "Connected"; readingsSingleUpdate( $hash, "state", $hash->{ConnectionState}, 1 ) if( $hash->{ConnectionState} ne ReadingsVal($hash->{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; $hash->{helper}{PARTIAL} = ""; harmony_send($hash, ""); } 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"; syswrite $hash->{CD}, $data; } my $id = 0; sub harmony_sendIq($$;$) { my ($hash, $xml, $type) = @_; $type = 'get' if ( !$type ); ++$id; my $iq = "$xml"; $iq = "$xml"; harmony_send($hash,$iq); } sub harmony_sendPair($$) { my ($hash, $payload) = @_; $payload = '' if ( !$payload ); my $xml = "$payload"; harmony_sendIq($hash,$xml); } sub harmony_sendEngineGet($$;$) { my ($hash, $endpoint, $payload) = @_; $payload = '' if ( !$payload ); my $xml = "$payload"; 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 = "{'code':'$code'}"; harmony_sendIq($hash,$xml); } sub harmony_sendEngineRender($$$) { my ($hash, $endpoint, $payload) = @_; #my $xml = "$payload"; my $xml = "$payload"; harmony_sendIq($hash,$xml, "render"); } sub harmony_ping($) { my( $hash ) = @_; return if( $hash->{ConnectionState} eq "Disconnected" ); ++$id; harmony_send($hash, ""); 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; if( harmony_isFritzBox() ) { $json = decode_json($data); } else { $json = JSON->new->utf8(0)->decode($data); } if( $param->{type} eq 'token' ) { harmony_parseToken($hash,$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(AttrVal($defs{$d}{NAME},"disable",undef)); #} 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: device '$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( 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; if( harmony_isFritzBox() ) { $action = decode_json($function->{action}); } else { $action = JSON->new->utf8(0)->decode($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 = ""; 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; 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; 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"; } } $list .= " currentActivity:noArg"; 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; } 1; =pod =begin html

harmony

    Defines a device to integrate a Logitech Harmony Hub based remote control into fhem.

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

    Notes:
    • JSON has to be installed on the FHEM host.
    • For hubs with firmware version 3.x.y <username> and <password> are not required as no authentication with the logitech myharmony server is needed for the full functionality of this module.
    • For hubs with firmware version 4.x.y <username> and <password> are required for device level control. Activit level control is (currently) still possible without authentication.
    • 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).

    Define
      define <name> harmony [<username> <password>] <ip>

      Defines a harmony device.

      Examples:
        define hub harmony 10.0.1.4

    Readings
    • currentActivity
      the name of the currently selected activity.
    • previousActivity
      the name of the previous selected activity. does not trigger an event.
    • newVersion
      will be set if a new firmware version is avaliable.

    Internals
    • currentActivityID
      the id of the currently selected activity.
    • previousActivityID
      the id of the previous selected activity.
    • sleeptimer
      timeout for sleeptimer if any is set.

    Set
    • activity <id>|<name>
      switch to this activity
    • channel <channel>
      switch to <channel> in the current activity
    • command [<id>|<name>] <command>
      send the given ir command for the current activity or for the given device
    • getConfig
      request the configuration from the hub
    • getCurrentActivity
      request the current activity from the hub
    • off
      switch current activity off
    • reconnect [all]
      close connection to the hub and reconnect, if all is given also reconnect to the logitech server
    • sleeptimer [<timeout>]
      <timeout> -> timeout in minutes
      -1 -> timer off
      default -> 60 minutes
    • sync
      syncs the hub to the myHarmony config
    • hidDevice [<id>|<name>]
      sets the target device for keyboard commands, if no device is given -> set the target to the default device for the current activity.
    • text <text>
      sends <text> by bluetooth/smart keaboard dongle. a-z ,A-Z ,0-9, \n, \e, \t and space are currently possible
    • cursor <direction>
      moves the cursor by bluetooth/smart keaboard dongle. <direction> can be one of: up, down, left, right, pageUp, pageDown, home, end.
    • special <key>
      sends special key by bluetooth/smart keaboard dongle. <key> can be one of: previousTrack, nextTrack, stop, playPause, volumeUp, volumeDown, mute.
    • autocreate [<id>|<name>]
      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.
    • update
      triggers a firmware update. only available if a new firmware is available.
    The command, hidDevice, text, cursor and special commmands are also available for the autocreated devices. The <id>|<name> paramter hast to be omitted.

    Get
    • activites [<param>]
      lists all activities
      parm = power -> list power state for each device in activity
    • devices [<param>]
      lists all devices
    • commands [<id>|<name>]
      lists the commands for the specified activity or for all activities
    • deviceCommands [<id>|<name>]
      lists the commands for the specified device or for all devices
    • activityDetail [<id>|<name>]
    • deviceDetail [<id>|<name>]
    • configDetail
    • currentActivity
      returns the current activity name
    The commands commmand is also available for the autocreated devices. The <id>|<name> paramter hast to be omitted.

    Attributes
    • disable
      1 -> disconnect from the hub
=end html =cut