From bdc6e20c56e3e814fecad74b92ad717c798af32c Mon Sep 17 00:00:00 2001 From: LeonGaultier Date: Tue, 23 May 2017 19:22:29 +0000 Subject: [PATCH] 21_HEOS*: new modules to provide HEOS Multiroomsystem git-svn-id: https://svn.fhem.de/fhem/trunk@14352 2b470e98-0d58-463d-a4d8-8e2adae1ed80 --- fhem/CHANGED | 3 +- fhem/FHEM/21_HEOSGroup.pm | 795 +++++++++++++++++ fhem/FHEM/21_HEOSMaster.pm | 1655 ++++++++++++++++++++++++++++++++++++ fhem/FHEM/21_HEOSPlayer.pm | 1292 ++++++++++++++++++++++++++++ fhem/MAINTAINER.txt | 3 + 5 files changed, 3747 insertions(+), 1 deletion(-) create mode 100644 fhem/FHEM/21_HEOSGroup.pm create mode 100644 fhem/FHEM/21_HEOSMaster.pm create mode 100644 fhem/FHEM/21_HEOSPlayer.pm diff --git a/fhem/CHANGED b/fhem/CHANGED index 6e74e68cc..42c9ae4ea 100644 --- a/fhem/CHANGED +++ b/fhem/CHANGED @@ -1,7 +1,8 @@ # Add changes at the top of the list. Keep it in ASCII, and 80-char wide. # Do not insert empty lines here, update check depends on it. + - new 21_HEOS*: new modules to provide HEOS Multiroomsystem - bugfix 93_DbRep: commandref corrected - - change: SubProcess.pm: buffer reads, messages amended + - change: SubProcess.pm: buffer reads, messages amended - feature: 98_alarmclock: New feature PresenceDevice - change: 49_SSCam: version 2.2.1, last record playback possible as iFrame, deviceoverview available, diff --git a/fhem/FHEM/21_HEOSGroup.pm b/fhem/FHEM/21_HEOSGroup.pm new file mode 100644 index 000000000..03c0cb14a --- /dev/null +++ b/fhem/FHEM/21_HEOSGroup.pm @@ -0,0 +1,795 @@ +############################################################################### +# +# Developed with Kate +# +# (c) 2017 Copyright: Marko Oldenburg (leongaultier at gmail dot com) +# All rights reserved +# +# Special thanks goes to comitters: +# - Olaf Schnicke Thanks for many many Code +# - Dieter Hehlgans Thanks for 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$ +# +############################################################################### + +package main; + +use strict; +use warnings; +use JSON qw(decode_json); +use Encode qw(encode_utf8); + + +my $version = "0.2.0"; + + + + +# Declare functions +sub HEOSGroup_Initialize($); +sub HEOSGroup_Define($$); +sub HEOSGroup_Undef($$); +sub HEOSGroup_Attr(@); +sub HEOSGroup_Notify($$); +sub HEOSGroup_Parse($$); +sub HEOSGroup_WriteReadings($$); +sub HEOSGroup_Set($$@); +sub HEOSGroup_PreProcessingReadings($$); +sub HEOSGroup_GetGroupInfo($); +sub HEOSGroup_GetGroupVolume($); +sub HEOSGroup_GetGroupMute($); + + + + +sub HEOSGroup_Initialize($) { + + my ($hash) = @_; + + $hash->{Match} = '.*{"command":."group.*|.*{"command":."event\/group.*'; + + + # Provider + $hash->{SetFn} = "HEOSGroup_Set"; + $hash->{DefFn} = "HEOSGroup_Define"; + $hash->{UndefFn} = "HEOSGroup_Undef"; + $hash->{NotifyFn} = "HEOSGroup_Notify"; + $hash->{AttrFn} = "HEOSGroup_Attr"; + $hash->{ParseFn} = "HEOSGroup_Parse"; + $hash->{AttrList} = "IODev ". + "disable:1 ". + $readingFnAttributes; + + foreach my $d(sort keys %{$modules{HEOSGroup}{defptr}}) { + + my $hash = $modules{HEOSGroup}{defptr}{$d}; + $hash->{VERSION} = $version; + } +} + +sub HEOSGroup_Define($$) { + + my ( $hash, $def ) = @_; + my @a = split( "[ \t]+", $def ); + splice( @a, 1, 1 ); + my $iodev; + my $i = 0; + + + foreach my $param ( @a ) { + if( $param =~ m/IODev=([^\s]*)/ ) { + + $iodev = $1; + splice( @a, $i, 3 ); + last; + } + + $i++; + } + + return "too few parameters: define HEOSGroup " if( @a < 2 ); + + my ($name,$gid) = @a; + + $hash->{GID} = $gid; + $hash->{VERSION} = $version; + $hash->{NOTIFYDEV} = "HEOSPlayer".abs($gid); + AssignIoPort($hash,$iodev) if( !$hash->{IODev} ); + + if(defined($hash->{IODev}->{NAME})) { + + Log3 $name, 3, "HEOSGroup ($name) - I/O device is " . $hash->{IODev}->{NAME}; + + } else { + + Log3 $name, 1, "HEOSGroup ($name) - no I/O device"; + } + + $iodev = $hash->{IODev}->{NAME}; + my $code = abs($gid); + + $code = $iodev."-".$code if( defined($iodev) ); + my $d = $modules{HEOSGroup}{defptr}{$code}; + + return "HEOSGroup device $hash->{GID} on HEOSMaster $iodev already defined as $d->{NAME}." + if( defined($d) && $d->{IODev} == $hash->{IODev} && $d->{NAME} ne $name ); + + Log3 $name, 3, "HEOSGroup ($name) - defined with Code: $code"; + + $attr{$name}{room} = "HEOS" if( !defined( $attr{$name}{room} ) ); + $attr{$name}{devStateIcon} = "on:10px-kreis-gruen off:10px-kreis-rot" if( !defined( $attr{$name}{devStateIcon} ) ); + + if( $init_done ) { + + InternalTimer( gettimeofday()+int(rand(2)), "HEOSGroup_GetGroupInfo", $hash, 0 ); + InternalTimer( gettimeofday()+int(rand(4)), "HEOSGroup_GetGroupVolume", $hash, 0 ); + InternalTimer( gettimeofday()+int(rand(6)), "HEOSGroup_GetGroupMute", $hash, 0 ); + + } else { + + InternalTimer( gettimeofday()+15+int(rand(2)), "HEOSGroup_GetGroupInfo", $hash, 0 ); + InternalTimer( gettimeofday()+15+int(rand(4)), "HEOSGroup_GetGroupVolume", $hash, 0 ); + InternalTimer( gettimeofday()+15+int(rand(6)), "HEOSGroup_GetGroupMute", $hash, 0 ); + } + + readingsBeginUpdate($hash); + readingsBulkUpdate($hash, 'state','Initialized'); + readingsBulkUpdate($hash, 'volumeUp', 5); + readingsBulkUpdate($hash, 'volumeDown', 5); + readingsEndUpdate($hash, 1); + + $modules{HEOSGroup}{defptr}{$code} = $hash; + return undef; +} + +sub HEOSGroup_Undef($$) { + + my ( $hash, $arg ) = @_; + my $name = $hash->{NAME}; + + + RemoveInternalTimer($hash); + my $code = abs($hash->{GID}); + $code = $hash->{IODev}->{NAME} ."-". $code if( defined($hash->{IODev}->{NAME}) ); + delete($modules{HEOSGroup}{defptr}{$code}); + + Log3 $name, 3, "HEOSGroup ($name) - device $name deleted with Code: $code"; + return undef; +} + +sub HEOSGroup_Attr(@) { + + my ( $cmd, $name, $attrName, $attrVal ) = @_; + my $hash = $defs{$name}; + my $token = $hash->{IODev}->{TOKEN}; + + + if( $attrName eq "disable" ) { + if( $cmd eq "set" and $attrVal eq "1" ) { + + readingsSingleUpdate ( $hash, "state", "disabled", 1 ); + Log3 $name, 3, "HEOSGroup ($name) - disabled"; + + } elsif( $cmd eq "del" ) { + + readingsSingleUpdate ( $hash, "state", "active", 1 ); + Log3 $name, 3, "HEOSGroup ($name) - enabled"; + } + } + + if( $attrName eq "disabledForIntervals" ) { + if( $cmd eq "set" ) { + + Log3 $name, 3, "HEOSGroup ($name) - enable disabledForIntervals"; + readingsSingleUpdate ( $hash, "state", "Unknown", 1 ); + + } elsif( $cmd eq "del" ) { + + readingsSingleUpdate ( $hash, "state", "active", 1 ); + Log3 $name, 3, "HEOSGroup ($name) - delete disabledForIntervals"; + } + } +} + +sub HEOSGroup_Notify($$) { + + my ($hash,$dev) = @_; + my $name = $hash->{NAME}; + + + return undef if(IsDisabled($name)); + + my $events = deviceEvents($dev,1); + + return if( !$events ); + readingsBeginUpdate($hash); + + my %playerEevents = map { my ( $key, $value ) = split /:\s/; ( $key, $value ) } @$events; + + foreach my $key ( keys %playerEevents ) { + + #### playing Infos + readingsBulkUpdate( $hash, $key, $playerEevents{$key} ) if( grep { $_ =~ /$key/ } ("channel", "currentAlbum", "currentArtist", "currentImageUrl", "currentMedia", "currentMid", "currentQid", "currentSid", "currentStation", "currentTitle", "error", "playStatus", "repeat", "shuffle" ) ); + } + + readingsEndUpdate( $hash, 1 ); +} + +sub HEOSGroup_Set($$@) { + + my ($hash, $name, @aa) = @_; + my ($cmd, @args) = @aa; + my $gid = $hash->{GID}; + my $action; + my $heosCmd; + my $rvalue; + my $favorit; + my $favoritcount = 1; + my $string = "gid=$gid"; + + + #senden von Befehlen unterdrücken solange state nicht on ist + return undef unless ( ReadingsVal($name, "state", "off") eq "on" ); + + if( $cmd eq 'getGroupInfo' ) { + return "usage: $cmd" if( @args != 0 ); + + $heosCmd = $cmd; + + } elsif( $cmd eq 'mute' ) { + my $param = "on|off"; + return "usage: $cmd $param" if( @args != 1 || ! grep { $_ =~ /$args[0]/ } split(/\|/, $param) ); + + $heosCmd = 'setGroupMute'; + $action = "state=$args[0]"; + + } elsif( $cmd eq 'volume' ) { + return "usage: $cmd 0-100" if( @args != 1 || $args[0] !~ /(\d+)/ || $args[0] > 100 || $args[0] < 0 ); + + $heosCmd = 'setGroupVolume'; + $action = "level=$args[0]"; + + } elsif( $cmd eq 'volumeUp' ) { + return "usage: $cmd 0-10" if( @args != 1 || $args[0] !~ /(\d+)/ || $args[0] > 10 || $args[0] < 1 ); + + $heosCmd = 'GroupVolumeUp'; + $action = "step=$args[0]"; + + } elsif( $cmd eq 'volumeDown' ) { + return "usage: $cmd 0-10" if( @args != 1 || $args[0] !~ /(\d+)/ || $args[0] > 10 || $args[0] < 1 ); + + $heosCmd = 'groupVolumeDown'; + $action = "step=$args[0]"; + + } elsif( $cmd eq 'clearGroup' ) { + return "usage: $cmd" if( @args != 0 ); + + $heosCmd = 'createGroup'; + $string = "pid=$gid"; + + } elsif( grep { $_ eq $cmd } ("play", "stop", "pause", "next", "prev", "channel", "channelUp", "channelDown", "playlist" ) ) { + + #ab hier Playerbefehle emuliert + $string = "pid=$gid"; + + if( $cmd eq 'repeat' ) { + return "usage: repeat one,all,off" if( @args != 1 ); + + $heosCmd = 'setPlayMode'; + $rvalue = 'on_'.$args[0]; + $rvalue = 'off' if($rvalue eq 'on_off'); + $action = "repeat=$rvalue&shuffle=".ReadingsVal($name,'shuffle','off'); + + } elsif( $cmd eq 'shuffle' ) { + return "usage: shuffle on,off" if( @args != 1 ); + + $heosCmd = 'setPlayMode'; + $rvalue = 'on_'.ReadingsVal($name,'repeat','off'); + $rvalue = 'off' if($rvalue eq 'on_off'); + $action = "repeat=$rvalue&shuffle=$args[0]"; + + } elsif( $cmd eq 'play' ) { + return "usage: play" if( @args != 0 ); + + $heosCmd = 'setPlayState'; + $action = "state=$cmd"; + + } elsif( $cmd eq 'stop' ) { + return "usage: stop" if( @args != 0 ); + + $heosCmd = 'setPlayState'; + $action = "state=$cmd"; + + } elsif( $cmd eq 'pause' ) { + return "usage: pause" if( @args != 0 ); + + $heosCmd = 'setPlayState'; + $action = "state=$cmd"; + } elsif( $cmd eq 'next' ) { + return "usage: next" if( @args != 0 ); + + $heosCmd = 'playNext'; + + } elsif( $cmd eq 'prev' ) { + return "usage: prev" if( @args != 0 ); + + $heosCmd = 'playPrev'; + + } elsif ( $cmd =~ /channel/ ) { + + my $favorit = ReadingsVal($name,"channel", 1); + + $favoritcount = scalar(@{$hash->{IODev}{helper}{favorites}}) if ( defined $hash->{IODev}{helper}{favorites} ); + $heosCmd = 'playPresetStation'; + + if ( $cmd eq 'channel' ) { + return "usage: $cmd 1-$favoritcount" if( @args != 1 || $args[0] !~ /(\d+)/ || $args[0] > $favoritcount || $args[0] < 1); + + $action = "preset=$args[0]"; + + } elsif( $cmd eq 'channelUp' ) { + return "usage: $cmd" if( @args != 0 ); + + ++$favorit; + if ( $favorit > $favoritcount ) { + if ( AttrVal($name, 'channelring', 0) == 1 ) { + + $favorit = 1; + + } else { + + $favorit = $favoritcount; + + } + } + + $action = "preset=".$favorit; + + } elsif( $cmd eq 'channelDown' ) { + return "usage: $cmd" if( @args != 0 ); + + --$favorit; + if ( $favorit <= 0 ) { + if ( AttrVal($name, 'channelring', 0) == 1 ) { + + $favorit = $favoritcount; + + } else { + + $favorit = 1; + } + } + + $action = "preset=".$favorit; + } + + } elsif ( $cmd =~ /Playlist/ ) { + + my @cids = map { $_->{cid} } grep { $_->{name} =~ /\Q$args[0]\E/i } (@{ $hash->{IODev}{helper}{playlists} }); + + if ( scalar @args == 1 && scalar @cids > 0 ) { + if ( $cmd eq 'playPlaylist' ) { + + $heosCmd = $cmd; + $action = "sid=1025&cid=$cids[0]&aid=4"; + + } elsif ( $cmd eq 'deletePlaylist' ) { + + $heosCmd = $cmd; + $action = "cid=$cids[0]"; + $string = "sid=1025"; + } + } else { + + IOWrite($hash,'browseSource','sid=1025'); + my @playlists = map { $_->{name} } (@{ $hash->{IODev}{helper}{playlists}}); + return "usage: $cmd ".join(",",@playlists); + } + } + } else { + + my $list = "getGroupInfo:noArg mute:on,off volume:slider,0,5,100 volumeUp:slider,0,1,10 volumeDown:slider,0,1,10 clearGroup:noArg repeat:one,all,off shuffle:on,off play:noArg stop:noArg pause:noArg next:noArg prev:noArg channelUp:noArg channelDown:noArg "; + + $list .= " channel:slider,1,1,".scalar(@{$hash->{IODev}{helper}{favorites}}) if ( defined $hash->{IODev}{helper}{favorites} ); + + if ( defined $hash->{IODev}{helper}{playlists} ) { + + my @playlists = map { my %n; $n{name} = $_->{name}; $n{name} =~ s/\s+/\ /g; $n{name} } (@{ $hash->{IODev}{helper}{playlists}}); + $list .= " playlist:".join(",",@playlists) if( scalar @playlists > 0 ); + } + + return "Unknown argument $cmd, choose one of $list"; + } + + $string .= "&$action" if( defined($action)); + IOWrite($hash,"$heosCmd","$string"); + Log3 $name, 4, "HEOSGroup ($name) - IOWrite: $heosCmd $string IODevHash=$hash->{IODev}"; + return undef; +} + +sub HEOSGroup_Parse($$) { + + my ($io_hash,$json) = @_; + my $name = $io_hash->{NAME}; + my $gid; + my $decode_json; + my $code; + + + $decode_json = decode_json(encode_utf8($json)); + Log3 $name, 4, "HEOSGroup ($name) - ParseFn wurde aufgerufen"; + + if( defined($decode_json->{gid}) ) { + + $gid = $decode_json->{gid}; + $code = abs($gid); + $code = $io_hash->{NAME} ."-". $code if( defined($io_hash->{NAME}) ); + + + if( my $hash = $modules{HEOSGroup}{defptr}{$code} ) { + + IOWrite($hash,'getGroupInfo',"gid=$hash->{GID}"); + readingsSingleUpdate( $hash, "state", "on", 1 ); + Log3 $hash->{NAME}, 4, "HEOSGroup ($hash->{NAME}) - find logical device: $hash->{NAME}"; + Log3 $hash->{NAME}, 4, "HEOSGroup ($hash->{NAME}) - find GID in root from decode_json"; + return $hash->{NAME}; + + } else { + + my $devname = "HEOSGroup".abs($gid); + return "UNDEFINED $devname HEOSGroup $gid IODev=$name"; + } + + } else { + + my %message = map { my ( $key, $value ) = split "="; $key => $value } split('&', $decode_json->{heos}{message}); + + $gid = $message{pid} if( defined($message{pid}) ); + $gid = $message{gid} if( defined($message{gid}) ); + $gid = $decode_json->{payload}{gid} if( defined($decode_json->{payload}{gid}) ); + + Log3 $name, 4, "HEOSGroup ($name) - GID: $gid"; + + $code = abs($gid); + $code = $io_hash->{NAME} ."-". $code if( defined($io_hash->{NAME}) ); + + if( my $hash = $modules{HEOSGroup}{defptr}{$code} ) { + + HEOSGroup_WriteReadings($hash,$decode_json); + Log3 $hash->{NAME}, 4, "HEOSGroup ($hash->{NAME}) - find logical device: $hash->{NAME}"; + return $hash->{NAME}; + + } else { + + my $devname = "HEOSGroup".abs($gid); + return "UNDEFINED $devname HEOSGroup $gid IODev=$name"; + } + } +} + +sub HEOSGroup_WriteReadings($$) { + + my ($hash,$decode_json) = @_; + my $name = $hash->{NAME}; + + + Log3 $name, 4, "HEOSGroup ($name) - processing data to write readings"; + ############################ + #### Aufbereiten der Daten soweit nötig (bei Events zum Beispiel) + my $readingsHash = HEOSGroup_PreProcessingReadings($hash,$decode_json) + if( $decode_json->{heos}{message} =~ /^gid=/ ); + + ############################ + #### schreiben der Readings + readingsBeginUpdate($hash); + ### Event Readings + if( ref($readingsHash) eq "HASH" ) { + + Log3 $name, 4, "HEOSGroup ($name) - response json Hash back from HEOSGroup_PreProcessingReadings"; + my $t; + my $v; + + while( ( $t, $v ) = each (%{$readingsHash}) ) { + + readingsBulkUpdate( $hash, $t, $v ) if( defined( $v ) ); + } + } + + #readingsBulkUpdate( $hash, 'state', 'on' ); + ### GroupInfos + readingsBulkUpdate( $hash, 'name', $decode_json->{payload}{name} ); + readingsBulkUpdate( $hash, 'gid', $decode_json->{payload}{gid} ); + + if ( ref($decode_json->{payload}{players}) eq "ARRAY" ) { + + my @members; + + foreach my $player (@{ $decode_json->{payload}{players} }) { + + readingsBulkUpdate( $hash, 'leader', $player->{name} ) if ( $player->{role} eq "leader" ); + push( @members, $player->{name}) if ( $player->{role} eq "member" ); + } + + if ( scalar @members > 1 ) { + + readingsBulkUpdate( $hash, 'member', join(",",@members) ); + + } else { + + readingsBulkUpdate( $hash, 'member', $members[0] ); + } + } + + readingsEndUpdate( $hash, 1 ); + + Log3 $name, 5, "HEOSGroup ($name) - readings set for $name"; + return undef; +} + +############### +### my little Helpers + +sub HEOSGroup_PreProcessingReadings($$) { + + my ($hash,$decode_json) = @_; + my $name = $hash->{NAME}; + my $reading; + my %buffer; + my %message = map { my ( $key, $value ) = split "="; $key => $value } split('&', $decode_json->{heos}{message}); + + + Log3 $name, 4, "HEOSGroup ($name) - preprocessing readings"; + if ( $decode_json->{heos}{command} =~ /volume_changed/ or $decode_json->{heos}{command} =~ /set_volume/ or $decode_json->{heos}{command} =~ /get_volume/ ) { + + my @value = split('&', $decode_json->{heos}{message}); + + $buffer{'volume'} = substr($value[1],6); + $buffer{'mute'} = substr($value[2],5) if( $decode_json->{heos}{command} =~ /volume_changed/ ); + + } elsif ( $decode_json->{heos}{command} =~ /volume_up/ or $decode_json->{heos}{command} =~ /volume_down/ ) { + + my @value = split('&', $decode_json->{heos}{message}); + + $buffer{'volumeUp'} = substr($value[1],5) if( $decode_json->{heos}{command} =~ /volume_up/ ); + $buffer{'volumeDown'} = substr($value[1],5) if( $decode_json->{heos}{command} =~ /volume_down/ ); + + } elsif ( $decode_json->{heos}{command} =~ /get_mute/ ) { + + my @value = split('&', $decode_json->{heos}{message}); + + $buffer{'mute'} = substr($value[1],6); + + } else { + + Log3 $name, 4, "HEOSGroup ($name) - no match found"; + return undef; + } + + Log3 $name, 4, "HEOSGroup ($name) - Match found for decode_json"; + return \%buffer; +} + +sub HEOSGroup_GetGroupInfo($) { + + my $hash = shift; + + + RemoveInternalTimer($hash,'HEOSGroup_GetGroupInfo'); + IOWrite($hash,'getGroupInfo',"gid=$hash->{GID}"); +} + +sub HEOSGroup_GetGroupVolume($) { + + my $hash = shift; + + + RemoveInternalTimer($hash,'HEOSGroup_GetGroupVolume'); + IOWrite($hash,'getGroupVolume',"gid=$hash->{GID}"); +} + +sub HEOSGroup_GetGroupMute($) { + + my $hash = shift; + + + RemoveInternalTimer($hash,'HEOSGroup_GetGroupMute'); + IOWrite($hash,'getGroupMute',"gid=$hash->{GID}"); +} + + + + + + +1; + + + + + +=pod +=item device +=item summary Modul to controls the Denon multiroom soundsystem +=item summary_DE Modul zum steuern des Denon Multiroom-Soundsystem + +=begin html + + +

HEOSGroup

+
    + HEOSGroup +

    + In combination with HEOSMaster and HEOSPlayer this FHEM Module controls the Denon multiroom soundsystem using a telnet socket connection and the HEOS Command Line Interface (CLI). +

    + Once the master device is created, the players and groups of Your system are automatically recognized and created in FHEM. From now on the players and groups can be controlled and changes in the HEOS app or at the Receiver are synchronized with the state and media readings of the players and groups. +
    +
    + Groups can be created from a player with "groupWithMember". +

    + Example: +

      + set living groupWithMember kitchen
      +
    +
    + ... creates a group named "living+kitchen" with player "living" as leader and player "kitchen" as member. + +

    + +

    + Readings +
      +
    • channel - nr of now playing favorite
    • +
    • currentAlbum - name of now playing album
    • +
    • currentArtist - name of now playing artist
    • +
    • currentImageUrl - URL of cover art, station logo, etc.
    • +
    • currentMedia - type of now playing media (song|station|genre|artist|album|container)
    • +
    • currentMid - media ID
    • +
    • currentQid - queue ID
    • +
    • currentSid - source ID
    • +
    • currentStation - name of now playing station
    • +
    • currentTitle - name of now playing title
    • +
    • error - last error
    • +
    • gid - group ID
    • +
    • leader - leader of the group
    • +
    • member - member(s) of the group
    • +
    • mute - player mute state (on|off)
    • +
    • name - name of player (received from app)
    • +
    • playStatus - state of player (play|pause|stop)
    • +
    • repeat - player repeat state (on_all|on_one|off)
    • +
    • shuffle - player shuffle state (on|off)
    • +
    • state - state of player connection (on|off)
    • +
    • volume - player volume level (0-100)
    • +
    • volumeDown - player volume step level (1-10, default 5)
    • +
    • volumeUp - player volume step level (1-10, default 5)
    • +
    +

    + + set +
      +
    • channel <nr> - plays favorite <nr> created with app
    • +
    • channelUp - switches to next favorite
    • +
    • channelDown- switches to previous favorite
    • +
    • clearGroup - dissolves the group (sets state to off)
    • +
    • getGroupInfo - get media info of the group
    • +
    • mute on|off - set mute state on|off
    • +
    • next - play next title in queue
    • +
    • pause - set state of player to "pause"
    • +
    • play - set state of player to "play"
    • +
    • playPlaylist <myList> - play playlist <myList>
    • +
    • prev - play previous title in queue
    • +
    • repeat - set player repeat state (on_all|on_one|off)
    • +
    • shuffle - set player shuffle state on|off
    • +
    • stop - set state of player to "stop"
    • +
    • volume - set volume 0..100
    • +
    • volumeDown - reduce volume by <volumeDown>
    • +
    • volumeUp - increase volume by <volumeUp>
    • +
    +

    + + state +
      +
    • state of group (on|off)
    • +
    +
+ +=end html + +=begin html_DE + + +

HEOSGroup

+
    + HEOSGroup +

    + In Kombination mit HEOSMaster and HEOSPlayer steuert dieses FHEM Modul das Denon Multiroom-Soundsystem mit Hilfe einer telnet Socket-Verbindung und dem HEOS Command Line Interface (CLI). +

    + Nachdem der Master einmal angelegt ist werden die Player und Gruppierungen des Systems automatisch erkannt und in FHEM angelegt. Von da an können die Player und Gruppierungen gesteuert werden und Veränderungen in der HEOS App oder am Reveiver werden mit dem Status und den Media Readings der Player und Gruppierungen synchronisiert. +
    +
    + Gruppierungen können aus einem Player heraus mit "groupWithMember" erzeugt werden. +

    + Beispiel: +

      + set Wohnzimmer groupWithMember Küche
      +
    +
    + ... erzeugt eine Gruppierung namens "Wohnzimmer+Küche" mit dem Player "Wohnzimmer" als Leader und dem Player "Küche" als Mitglied. + +

    + Readings +
      +
    • channel - Nr des gerade abgespielten Favoriten
    • +
    • currentAlbum - Name des gerade abgespielten Albums
    • +
    • currentArtist - Name des gerade abgespielten Künstlers
    • +
    • currentImageUrl - URL des Albumcovers, Senderlogos, etc.
    • +
    • currentMedia - Medientyp des gerade abgespielten Streams (song|station|genre|artist|album|container)
    • +
    • currentMid - media ID
    • +
    • currentQid - queue ID
    • +
    • currentSid - source ID
    • +
    • currentStation - Name des gerade abgespielten Senders
    • +
    • currentTitle - Name des gerade abgespielten Titels
    • +
    • error - letzte Fehlermeldung
    • +
    • gid - Gruppen-ID
    • +
    • leader - Leader der Gruppierung
    • +
    • member - Mitglied(er) der Gruppierung
    • +
    • mute - Player mute Status (on|off)
    • +
    • name - Name der Gruppierung
    • +
    • playStatus - Status des Players (play|pause|stop)
    • +
    • repeat - Player Repeat Status (on_all|on_one|off)
    • +
    • shuffle - Player Shuffle Status (on|off)
    • +
    • state - Status der Player-Verbindung (on|off)
    • +
    • volume - aktuelle Lautstärke (0-100)
    • +
    • volumeDown - Schrittweite Lautstärke (1-10, default 5)
    • +
    • volumeUp - Schrittweite Lautstärke (1-10, default 5)
    • +
    +

    + + set +
      +
    • channel <nr> - spielt den vorher mit der App erstellten Favoriten <nr> ab
    • +
    • channelUp - schaltet auf den nächsten Favoriten in der Favoritenliste um
    • +
    • channelDown- schaltet auf vorherigen Favoriten in der Favoritenliste um
    • +
    • clearGroup - Auflösen der Gruppierung (setzt state auf off)
    • +
    • getGroupInfo - holt die Media-Informationen der Gruppierung
    • +
    • mute on|off - setze den mute Status on|off
    • +
    • next - spielt nächsten Titel in Warteschlange
    • +
    • pause - setzt den Status des Players auf "pause"
    • +
    • play - setzt den Status des Players auf "play"
    • +
    • playPlaylist <myList> - spielt die Playlist <myList> ab
    • +
    • prev - spielt vorherigen Titel in Warteschlange
    • +
    • repeat - setzt den Player Repeat Status (on_all|on_one|off)
    • +
    • shuffle - setzt den Player Shuffle Status auf on|off
    • +
    • stop - setzt den Status des Players auf "stop"
    • +
    • volume - setzt die Lautstärke auf 0..100
    • +
    • volumeDown - verringert die Lautstärke um <volumeDown>
    • +
    • volumeUp - erhöht die Lautstärke um <volumeUp>
    • +
    +

    + + state +
      +
    • Status der Gruppierung (on|off)
    • +
    +
+ +=end html_DE + +=cut diff --git a/fhem/FHEM/21_HEOSMaster.pm b/fhem/FHEM/21_HEOSMaster.pm new file mode 100644 index 000000000..0d560de88 --- /dev/null +++ b/fhem/FHEM/21_HEOSMaster.pm @@ -0,0 +1,1655 @@ +############################################################################### +# +# Developed with Kate +# +# (c) 2017 Copyright: Marko Oldenburg (leongaultier at gmail dot com) +# All rights reserved +# +# Special thanks goes to comitters: +# - Olaf Schnicke Thanks for many many Code +# - Dieter Hehlgans Thanks for 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$ +# +############################################################################### + +################################# +######### Wichtige Hinweise und Links ################# +# +## Das JSON Modul immer in einem eval aufrufen +# $data = eval{decode_json($data)}; +# +# if($@){ +# Log3($SELF, 2, "$TYPE ($SELF) - error while request: $@"); +# +# readingsSingleUpdate($hash, "state", "error", 1); +# +# return; +# } +## +## +## +# + +################################ + +package main; + +use strict; +use warnings; +#use Data::Dumper; + +my $missingModul = ""; + +eval "use Net::Telnet;1" or $missingModul .= "Net::Telnet "; +eval "use JSON;1" or $missingModul .= "JSON "; +eval "use Encode;1" or $missingModul .= "Encode "; +eval "use IO::Socket::Multicast;1" or $missingModul .= "IO::Socket::Multicast "; + + + + +my $version = "0.2.0"; + +my %heosCmds = ( + 'enableChangeEvents' => 'system/register_for_change_events?enable=', + 'checkAccount' => 'system/check_account', + 'signAccountIn' => 'system/sign_in?', + 'signAccountOut' => 'system/sign_out', + 'reboot' => 'system/reboot', + 'getMusicSources' => 'browse/get_music_sources', + 'browseSource' => 'browse/browse?', + 'getPlayers' => 'player/get_players', + 'getGroups' => 'group/get_groups', + 'getPlayerInfo' => 'player/get_player_info?', + 'getGroupInfo' => 'group/get_group_info?', + 'getPlayState' => 'player/get_play_state?', + 'getPlayMode' => 'player/get_play_mode?', + 'getMute' => 'player/get_mute?', + 'getGroupMute' => 'group/get_mute?', + 'getQueue' => 'player/get_queue?', + 'playQueueItem' => 'player/play_queue?', + 'clearQueue' => 'player/clear_queue?', + 'saveQueue' => 'player/save_queue?', + 'getVolume' => 'player/get_volume?', + 'getGroupVolume' => 'group/get_volume?', + 'setPlayState' => 'player/set_play_state?', + 'setPlayMode' => 'player/set_play_mode?', + 'setMute' => 'player/set_mute?', + 'setGroupMute' => 'group/set_mute?', + 'playNext' => 'player/play_next?', + 'playPrev' => 'player/play_prev?', + 'playPresetStation' => 'browse/play_preset?', + 'playInput' => 'browse/play_input?', + 'playStream' => 'browse/play_stream?', + 'playPlaylist' => 'browse/add_to_queue?', + 'renamePlaylist' => 'browse/rename_playlist?', + 'deletePlaylist' => 'browse/delete_playlist?', + 'setVolume' => 'player/set_volume?', + 'setGroupVolume' => 'group/set_volume?', + 'volumeUp' => 'player/volume_up?', + 'volumeDown' => 'player/volume_down?', + 'GroupVolumeUp' => 'group/volume_up?', + 'GroupVolumeDown' => 'group/volume_down?', + 'getNowPlayingMedia' => 'player/get_now_playing_media?', + 'eventChangeVolume' => 'event/player_volume_changed', + 'createGroup' => 'group/set_group?', + 'searchCriteria' => 'browse/get_search_criteria?', + 'search' => 'browse/search?' +); + + +# Declare functions +sub HEOSMaster_Initialize($); +sub HEOSMaster_Define($$); +sub HEOSMaster_Undef($$); +sub HEOSMaster_Set($@); +sub HEOSMaster_Open($); +sub HEOSMaster_Close($); +sub HEOSMaster_Read($); +sub HEOSMaster_Write($@); +sub HEOSMaster_Attr(@); +sub HEOSMaster_FirstRun($); +sub HEOSMaster_ResponseProcessing($$); +sub HEOSMaster_WriteReadings($$); +sub HEOSMaster_GetPlayers($); +sub HEOSMaster_EnableChangeEvents($); +sub HEOSMaster_PreProcessingReadings($$); +sub HEOSMaster_ReOpen($); +sub HEOSMaster_ReadPassword($); +sub HEOSMaster_StorePassword($$); +sub HEOSMaster_GetGroups($); +sub HEOSMaster_ProcessRead($$); +sub HEOSMaster_ParseMsg($$); +sub HEOSMaster_CheckAccount($); +sub HEOSMaster_Get($$@); +sub HEOSMaster_GetFavorites($); +sub HEOSMaster_GetHistory($); +sub HEOSMaster_GetInputs($); +sub HEOSMaster_GetMusicSources($); +sub HEOSMaster_GetPlaylists($); +sub HEOSMaster_GetServers($); +sub HEOSMaster_MakePlayLink($$$$$$$); +sub HEOSMaster_MakeImage($$); + + + + +sub HEOSMaster_Initialize($) { + + my ($hash) = @_; + + + # Provider + $hash->{ReadFn} = "HEOSMaster_Read"; + $hash->{WriteFn} = "HEOSMaster_Write"; + $hash->{Clients} = ":HEOSPlayer:"; + $hash->{MatchList} = { "1:HEOSPlayer" => '.*{"command":."player.*|.*{"command":."event\/player.*|.*{"command":."event\/repeat_mode_changed.*|.*{"command":."event\/shuffle_mode_changed.*|.*{"command":."event\/favorites_changed.*', + "2:HEOSGroup" => '.*{"command":."group.*|.*{"command":."event\/group.*' + }; + + # Consumer + $hash->{SetFn} = "HEOSMaster_Set"; + $hash->{GetFn} = "HEOSMaster_Get"; + $hash->{DefFn} = "HEOSMaster_Define"; + $hash->{UndefFn} = "HEOSMaster_Undef"; + $hash->{AttrFn} = "HEOSMaster_Attr"; + $hash->{AttrList} = "disable:1 ". + "heosUsername ". + $readingFnAttributes; + + foreach my $d(sort keys %{$modules{HEOSMaster}{defptr}}) { + + my $hash = $modules{HEOSMaster}{defptr}{$d}; + $hash->{VERSION} = $version; + } +} + +sub HEOSMaster_Define($$) { + + my ( $hash, $def ) = @_; + my @a = split( "[ \t][ \t]*", $def ); + + + return "too few parameters: define HEOSMaster " if( @a != 3 ); + return "Cannot define a HEOS device. Perl modul $missingModul is missing." if ( $missingModul ); + + my $name = $a[0]; + my $host = $a[2]; + + $hash->{HOST} = $host; + $hash->{VERSION} = $version; + + + Log3 $name, 3, "HEOSMaster ($name) - defined with host $host"; + $attr{$name}{room} = "HEOS" if( !defined( $attr{$name}{room} ) ); + + readingsBeginUpdate($hash); + readingsBulkUpdate($hash,'state','Initialized'); + readingsBulkUpdate($hash,'enableChangeEvents', 'off'); + readingsEndUpdate($hash,1); + + if( $init_done ) { + + HEOSMaster_FirstRun($hash); + + } else { + + InternalTimer( gettimeofday()+15, 'HEOSMaster_FirstRun', $hash, 0 ) if( ($hash->{HOST}) ); + } + + $modules{HEOSPlayer}{defptr}{$host} = $hash; + return undef; +} + +sub HEOSMaster_Undef($$) { + + my ( $hash, $arg ) = @_; + my $host = $hash->{HOST}; + my $name = $hash->{NAME}; + + + HEOSMaster_Close($hash); + delete $modules{HEOSMaster}{defptr}{$hash->{HOST}}; + + Log3 $name, 3, "HEOSPlayer ($name) - device $name deleted"; + return undef; +} + +sub HEOSMaster_Attr(@) { + + my ( $cmd, $name, $attrName, $attrVal ) = @_; + my $hash = $defs{$name}; + my $orig = $attrVal; + + + if( $attrName eq "disable" ) { + if( $cmd eq "set" and $attrVal eq "1" ) { + + readingsSingleUpdate ( $hash, "state", "disabled", 1 ); + Log3 $name, 3, "HEOSMaster ($name) - disabled"; + + } elsif( $cmd eq "del" ) { + + readingsSingleUpdate ( $hash, "state", "active", 1 ); + Log3 $name, 3, "HEOSMaster ($name) - enabled"; + } + } + + if( $attrName eq "disabledForIntervals" ) { + if( $cmd eq "set" ) { + + Log3 $name, 3, "HEOSMaster ($name) - enable disabledForIntervals"; + readingsSingleUpdate ( $hash, "state", "Unknown", 1 ); + + } elsif( $cmd eq "del" ) { + + readingsSingleUpdate ( $hash, "state", "active", 1 ); + Log3 $name, 3, "HEOSMaster ($name) - delete disabledForIntervals"; + } + } + + return undef; +} + +sub HEOSMaster_Get($$@) { + + my ($hash, $name, @aa) = @_; + my ($cmd, @args) = @aa; + my $pid = $hash->{PID}; + + + if( $cmd eq 'showAccount' ) { + return "usage: $cmd" if( @args != 0 ); + + return AttrVal($name,'heosUsername',0) . ":" .HEOSMaster_ReadPassword($hash); + } + + my $list = 'showAccount:noArg'; + return "Unknown argument $cmd, choose one of $list"; +} + +sub HEOSMaster_Set($@) { + + my ($hash, $name, $cmd, @args) = @_; + my ($arg, @params) = @args; + my $action; + my $heosCmd; + + + if($cmd eq 'reopen') { + return "usage: $cmd" if( @args != 0 ); + + HEOSMaster_ReOpen($hash); + return undef; + + } elsif($cmd eq 'getPlayers') { + return "usage: $cmd" if( @args != 0 ); + + $heosCmd = 'getPlayers'; + $action = undef; + + } elsif($cmd eq 'getGroups') { + return "usage: $cmd" if( @args != 0 ); + + $heosCmd = 'getGroups'; + $action = undef; + + } elsif($cmd eq 'enableChangeEvents') { + my $param = "on|off"; + return "usage: $cmd $param" if( @args != 1 || ! grep { $_ =~ /$args[0]/ } split(/\|/, $param) ); + + $heosCmd = $cmd; + $action = $args[0]; + + } elsif($cmd eq 'checkAccount') { + return "usage: $cmd" if( @args != 0 ); + + $heosCmd = $cmd; + $action = undef; + + } elsif($cmd eq 'signAccount') { + my $param = "In|Out"; + return "usage: $cmd $param" if( @args != 1 || ! grep { $_ =~ /$args[0]/ } split(/\|/, $param) ); + + return "please set account informattion first" if(AttrVal($name,'heosUsername','none') eq 'none'); + $heosCmd = $cmd . $args[0]; + $action = 'un='. AttrVal($name,'heosUsername','none') . '&pw=' . HEOSMaster_ReadPassword($hash) if($args[0] eq 'In'); + + } elsif($cmd eq 'password') { + return "usage: $cmd" if( @args != 1 ); + + return HEOSMaster_StorePassword( $hash, $args[0] ); + + } elsif($cmd eq 'reboot') { + return "usage: $cmd" if( @args != 0 ); + + $heosCmd = $cmd; + $action = undef; + + } else { + + my $list = ""; + $list .= "reopen:noArg getPlayers:noArg getGroups:noArg enableChangeEvents:on,off checkAccount:noArg signAccount:In,Out password reboot"; + return "Unknown argument $cmd, choose one of $list"; + } + + HEOSMaster_Write($hash,$heosCmd,$action,undef); +} + +sub HEOSMaster_Open($) { + + my $hash = shift; + my $name = $hash->{NAME}; + my $host = $hash->{HOST}; + my $port = 1255; + my $timeout = 0.1; + my $user = AttrVal($name,'heosUsername',undef); + my $password = HEOSMaster_ReadPassword($hash); + + + Log3 $name, 4, "HEOSMaster ($name) - Baue Socket Verbindung auf"; + + my $socket = new Net::Telnet ( Host=>$host, + Port => $port, + Timeout=>$timeout, + Errmode=>'return') + or return Log3 $name, 3, "HEOSMaster ($name) Couldn't connect to $host:$port"; + + $hash->{FD} = $socket->fileno(); + $hash->{CD} = $socket; # sysread / close won't work on fileno + $selectlist{$name} = $hash; + + readingsSingleUpdate($hash, 'state', 'connected', 1 ); + Log3 $name, 4, "HEOSMaster ($name) - Socket Connected"; + + #hinzugefügt laut Protokoll 2.1.1 Initsequenz + HEOSMaster_Write($hash,'enableChangeEvents','off',undef); + Log3 $name, 4, "HEOSMaster ($name) - set enableChangeEvents off"; + + #hinzugefügt laut Protokoll 2.1.1 Initsequenz + if( defined($user) and defined($password) ) { + + HEOSMaster_Write($hash,'signAccountIn',"un=$user&pw=$password",undef); + Log3 $name, 4, "HEOSMaster ($name) - sign in"; + } + + HEOSMaster_GetPlayers($hash); + InternalTimer( gettimeofday()+1, 'HEOSMaster_EnableChangeEvents', $hash, 0 ); + InternalTimer( gettimeofday()+2, 'HEOSMaster_GetMusicSources', $hash, 0 ); + InternalTimer( gettimeofday()+3, 'HEOSMaster_GetGroups', $hash, 0 ); + InternalTimer( gettimeofday()+10, 'HEOSMaster_GetPlayers', $hash, 0 ); +} + +sub HEOSMaster_Close($) { + + my $hash = shift; + my $name = $hash->{NAME}; + + + return if( !$hash->{CD} ); + + close($hash->{CD}) if($hash->{CD}); + delete($hash->{FD}); + delete($hash->{CD}); + delete($selectlist{$name}); + + readingsSingleUpdate($hash, 'state', 'not connected', 1 ); +} + +sub HEOSMaster_ReOpen($) { + + my $hash = shift; + my $name = $hash->{NAME}; + + + HEOSMaster_Close($hash); + HEOSMaster_Open($hash) if( !$hash->{CD} or !defined($hash->{CD}) ); +} + +sub HEOSMaster_Write($@) { + + my ($hash,$heosCmd,$value,$blocking) = @_; + my $name = $hash->{NAME}; + my $string = "heos://$heosCmds{$heosCmd}"; + + if( defined($value) ) { + + $string .= "${value}" if( $value ne '&' ); + } + + if ( defined $blocking ) { + + my $idx = $blocking->{cl}{LASTACCESS}; + $hash->{helper}{blocking}{$idx} = $blocking; + $string .= "&SEQUENCE=$idx"; + } + + $string .= "\r\n"; + Log3 $name, 4, "HEOSMaster ($name) - WriteFn called"; + + return Log3 $name, 4, "HEOSMaster ($name) - socket not connected" + unless($hash->{CD}); + + Log3 $name, 5, "HEOSMaster ($name) - $string"; + syswrite($hash->{CD}, $string); + + return undef; +} + +sub HEOSMaster_Read($) { + + my $hash = shift; + my $name = $hash->{NAME}; + my $len; + my $buf; + + + Log3 $name, 4, "HEOSMaster ($name) - ReadFn gestartet"; + $len = sysread($hash->{CD},$buf,1024); # die genaue Puffergröße wird noch ermittelt + + if( !defined($len) || !$len ) { + + Log3 $name, 5, "HEOSMaster ($name) - connection closed by remote Host"; + HEOSMaster_Close($hash); + return; + } + + unless( defined $buf) { + + Log3 $name, 3, "HEOSMaster ($name) - Keine Daten empfangen"; + return; + } + + Log3 $name, 5, "HEOSMaster ($name) - received buffer data, start HEOSMaster_ProcessRead: $buf"; + HEOSMaster_ProcessRead($hash,$buf); +} + +sub HEOSMaster_ProcessRead($$) { + + my ($hash, $data) = @_; + my $name = $hash->{NAME}; + my $buffer = ''; + + + Log3 $name, 4, "HEOSMaster ($name) - process read"; + #include previous partial message + + if(defined($hash->{PARTIAL}) && $hash->{PARTIAL}) { + + Log3 $name, 5, "HEOSMaster ($name) - PARTIAL: " . $hash->{PARTIAL}; + $buffer = $hash->{PARTIAL}; + + } else { + + Log3 $name, 4, "HEOSMaster ($name) - No PARTIAL buffer"; + } + + Log3 $name, 5, "HEOSMaster ($name) - Incoming data: " . $data; + + $buffer = $buffer . $data; + + Log3 $name, 5, "HEOSMaster ($name) - Current processing buffer (PARTIAL + incoming data): " . $buffer; + + my ($json,$tail) = HEOSMaster_ParseMsg($hash, $buffer); + #processes all complete messages + + while($json) { + + $hash->{LAST_RECV} = time(); + Log3 $name, 5, "HEOSMaster ($name) - Decoding JSON message. Length: " . length($json) . " Content: " . $json; + #my $obj = JSON->new->utf8(0)->decode($json); Änderung unter großem Vorbehalt wegen Sorge was Umlaute an geht!!! + my $obj = decode_json($json); + + if(defined($obj->{heos})) { + + HEOSMaster_ResponseProcessing($hash,$json); + Log3 $name, 4, "HEOSMaster ($name) - starte HEOSMaster_ResponseProcessing"; + + } elsif(defined($obj->{error})) { + + Log3 $name, 3, "HEOSMaster ($name) - Received error message: " . $json; + } + + ($json,$tail) = HEOSMaster_ParseMsg($hash, $tail); + } + + $hash->{PARTIAL} = $tail; + Log3 $name, 5, "HEOSMaster ($name) - Tail: " . $tail; + Log3 $name, 5, "HEOSMaster ($name) - PARTIAL: " . $hash->{PARTIAL}; + return; +} + +sub HEOSMaster_ResponseProcessing($$) { + + my ($hash,$json) = @_; + my $name = $hash->{NAME}; + my $decode_json; + my %message; + + + Log3 $name, 5, "HEOSMaster ($name) - JSON String: $json"; + return Log3 $name, 3, "HEOSMaster ($name) - empty answer received" + unless( defined($json)); + + Log3 $name, 4, "HEOSMaster ($name) - JSON detected!"; + $decode_json = decode_json(encode_utf8($json)); + + return Log3 $name, 3, "HEOSMaster ($name) - decode_json has no Hash" + unless(ref($decode_json) eq "HASH"); + + return Log3 $name, 4, "HEOSMaster ($name) - heos worked" + if( defined($decode_json->{heos}{message}) && $decode_json->{heos}{message} =~ /command\sunder\sprocess/ ); + + if( defined($decode_json->{heos}{result}) or $decode_json->{heos}{command} =~ /^system/ ) { + + HEOSMaster_WriteReadings($hash,$decode_json); + Log3 $name, 4, "HEOSMaster ($name) - call Sub HEOSMaster_WriteReadings"; + } + + if( defined($decode_json->{heos}{message}) ) { + + %message = map { my ( $key, $value ) = split "="; $key => $value } split('&', $decode_json->{heos}{message}); + + return Log3 $name, 4, "HEOSMaster ($name) - general error ID $message{eid} - $message{text}" + if( defined($message{eid}) ); + } + + #Player neu einlesen + if( $decode_json->{heos}{command} =~ /^event\/players_changed/ ) { + + HEOSMaster_Write($hash,'getPlayers',undef,undef); + return Log3 $name, 4, "HEOSMaster ($name) - player changed"; + } + + #Gruppen neu einlesen + if( $decode_json->{heos}{command} =~ /^event\/groups_changed/ ) { + + + HEOSMaster_Write($hash,'getGroups',undef,undef); + #Player neu einlesen da Stereopaare sonst nicht erkannt werden + InternalTimer( gettimeofday()+3, 'HEOSMaster_GetPlayers', $hash, 0 ); + return Log3 $name, 4, "HEOSMaster ($name) - groups changed"; + } + + #Quellen neu einlesen + if( $decode_json->{heos}{command} =~ /^event\/sources_changed/ ) { + + HEOSMaster_Write($hash,'getMusicSources',undef,undef); + return Log3 $name, 4, "HEOSMaster ($name) - source changed"; + } + + #User neu einlesen + if( $decode_json->{heos}{command} =~ /^event\/user_changed/ ) { + + HEOSMaster_Write($hash,'checkAccount',undef,undef); + return Log3 $name, 4, "HEOSMaster ($name) - user changed"; + } + + #Queue für Player neu einlesen + if ( $decode_json->{heos}{command} =~ /^event\/player_queue_changed/ ) { + + HEOSMaster_Write($hash,'getQueue',"pid=$message{pid}",undef); + return Log3 $name, 3, "HEOSMaster ($name) - queue changed"; + + } + + #Playlisten neu einlesen da Queue als Playlist gespeichert wurde + if( $decode_json->{heos}{command} =~ /^player\/save_queue/ ) { + + HEOSMaster_Write($hash,'browseSource','sid=1025',undef); + return Log3 $name, 4, "HEOSMaster ($name) - playlist changed"; + } + + if( $decode_json->{heos}{command} =~ /^browse\/get_music_sources/ and ref($decode_json->{payload}) eq "ARRAY" and scalar(@{$decode_json->{payload}}) > 0) { + + #liest nur die Onlinequellen der Rest wird extra eingelesen + $hash->{helper}{sources} = []; + my $i = 4; + + foreach my $payload ( @{$decode_json->{payload}} ) { + if( $payload->{sid} eq "1024" ) { + + $i += 2; + InternalTimer( gettimeofday()+$i, 'HEOSMaster_GetServers', $hash, 0 ); + Log3 $name, 4, "HEOSMaster ($name) - GetServers in $i seconds"; + + } elsif( $payload->{sid} eq "1025" ) { + + $i += 2; + InternalTimer( gettimeofday()+$i, 'HEOSMaster_GetPlaylists', $hash, 0 ); + Log3 $name, 4, "HEOSMaster ($name) - GetPlaylists in $i seconds"; + + } elsif( $payload->{sid} eq "1026" ) { + + $i += 2; + InternalTimer( gettimeofday()+$i, 'HEOSMaster_GetHistory', $hash, 0 ); + Log3 $name, 4, "HEOSMaster ($name) - GetHistory in $i seconds"; + + } elsif( $payload->{sid} eq "1027" ) { + + $i += 2; + InternalTimer( gettimeofday()+$i, 'HEOSMaster_GetInputs', $hash, 0 ); + Log3 $name, 4, "HEOSMaster ($name) - GetInputs in $i seconds"; + + } elsif( $payload->{sid} eq "1028" ) { + + $i += 2; + InternalTimer( gettimeofday()+$i, 'HEOSMaster_GetFavorites', $hash, 0 ); + Log3 $name, 4, "HEOSMaster ($name) - GetFavorites in $i seconds"; + + } else { + + #Onlinedienste + push( @{$hash->{helper}{sources}},$payload); + Log3 $name, 4, "HEOSMaster ($name) - GetRadioSource {$payload->{name} with sid $payload->{sid}"; + + foreach my $source (@{$hash->{helper}{sources}}) { + HEOSMaster_Write($hash,'searchCriteria','sid='.$source->{sid},undef); + } + } + } + + return Log3 $name, 3, "HEOSMaster ($name) - call Sourcebrowser"; + } + + if( $decode_json->{heos}{command} =~ /^browse\/browse/ and ref($decode_json->{payload}) eq "ARRAY" and scalar(@{$decode_json->{payload}}) > 0) { + if ( defined $message{sid} ) { + if ( defined $message{range} ) { + + $message{range} =~ s/(\d+)\,\d+/$1/; + + } else { + + $message{range} = 0; + } + + my $start = $message{range} + $message{returned}; + + if( $message{sid} eq '1024' ) { + + #Lokal einlesen + push( @{$hash->{helper}{sources}}, map { $_->{name} .= " USB" if ( $_->{sid} < 0 ); $_ } (@{$decode_json->{payload}}) ); + + foreach my $source (@{$hash->{helper}{sources}}) { + + HEOSMaster_Write($hash,'searchCriteria','sid='.$source->{sid},undef); + Log3 $name, 3, "HEOSMaster ($name) - call Browser for searchCriteria for sid $source->{sid}"; + } + + } elsif( $message{sid} eq '1025' ) { + + #Playlisten einlesen + $hash->{helper}{playlists} = [] if ( $message{range} == 0 ); + push( @{$hash->{helper}{playlists}}, (@{$decode_json->{payload}}) ); + + } elsif( $message{sid} eq '1026' ) { + + #History einlesen + $hash->{helper}{history} = [] if ( $message{range} == 0 ); + push( @{$hash->{helper}{history}}, (@{$decode_json->{payload}}) ); + + } elsif( $message{sid} eq '1027' ) { + + #Inputs einlesen + $hash->{helper}{aux} = [] if ( $message{range} == 0 ); + push( @{$hash->{helper}{aux}}, (@{$decode_json->{payload}}) ); + + foreach my $item (@{$decode_json->{payload}}) { + + + HEOSMaster_Write($hash,'browseSource',"sid=$item->{sid}",undef); + Log3 $name, 3, "HEOSMaster ($name) - call Browser for Input with sid $item->{sid}"; + } + + } elsif( $message{sid} eq '1028' ) { + + #Favoriten einlesen + $hash->{helper}{favorites} = [] if ( $message{range} == 0 ); + push( @{$hash->{helper}{favorites}}, (@{$decode_json->{payload}}) ); + + if ( $start >= $message{count} ) { + + #Nachricht an die Player das sich die Favoriten geändert haben + foreach my $dev ( devspec2array("TYPE=HEOSPlayer") ) { + + $json = '{"heos": {"command": "event/favorites_changed", "message": "pid='.$defs{$dev}->{PID}.'"}}'; + Dispatch($hash,$json,undef); + Log3 $name, 4, "HEOSMaster ($name) - call Dispatcher for Favorites Changed"; + } + } + + } else { + + #AUX Eingang des Player im Player abspeichern + if ( defined $hash->{helper}{aux} && grep( $_->{sid} =~ /^$message{sid}$/, (@{ $hash->{helper}{aux} }) ) ) { + + my $code = abs($message{sid}); + $code = $hash->{NAME} ."-". $code if( defined($hash->{NAME}) ); + + if( my $phash = $modules{HEOSPlayer}{defptr}{$code} ) { + + $phash->{helper}{aux} = $decode_json->{payload}; + } + + $json = '{"heos": {"command": "event/player_aux_changed", "message": "pid='.$message{sid}.'"}}'; + Dispatch($hash,$json,undef); + Log3 $name, 4, "HEOSMaster ($name) - call Dispatcher for AUX Changed"; + + } + + #aktuelle Medien einlesen + $hash->{helper}{media} = [] if ( $message{range} == 0 ); + push( @{$hash->{helper}{media}}, (@{$decode_json->{payload}}) ); + } + + Log3 $name, 4, "HEOSMaster ($name) - call Browser with sid $message{sid} and $message{returned} items from $message{count} items"; + + if ( $start < $message{count} ) { + + my $path = "sid=$message{sid}"; + $path .= "&cid=$message{cid}" if ( defined $message{cid} ); + $path .= "&SEQUENCE=$message{SEQUENCE}" if ( defined $message{SEQUENCE} ); + HEOSMaster_Write($hash,'browseSource',"$path&range=$start,".($start + 100),undef); + Log3 $name, 3, "HEOSMaster ($name) - call Browser with sid $message{sid} next Range from $message{returned}"; + + } else { + if ( defined $message{SEQUENCE} ) { + + my $idx = $message{SEQUENCE}; + if( defined $hash->{helper}{blocking}{$idx} && $hash->{helper}{blocking}{$idx}{cl}{canAsyncOutput} ) { + + my @list; + my $xcmd; + my $xtext; + my $ret; + + if( $hash->{helper}{blocking}{$idx}{cl}->{TYPE} eq 'FHEMWEB' ) { + + $ret = '
'; + $ret .= '

'.$hash->{helper}{blocking}{$idx}{sourcename}.'


'; + + } else { + + $ret .= $hash->{helper}{blocking}{$idx}{sourcename}."\n"; + $ret .= sprintf( "%-15s %s\n", 'key', 'title' ); + } + + if ( $message{sid} eq "1025" ) { + + @list = (@{$hash->{helper}{playlists}}); + + } elsif ( $message{sid} eq "1026" ) { + + @list = (@{$hash->{helper}{history}}); + + } elsif ( $message{sid} eq "1027" ) { + + @list = (@{$hash->{helper}{aux}}); + + } elsif ( $message{sid} eq "1028" ) { + + @list = (@{$hash->{helper}{favorites}}); + + } else { + + @list = (@{$hash->{helper}{media}}); + + } + + my $x = 0; + foreach my $item (@list) { + $ret .= HEOSMaster_MakePlayLink($hash->{helper}{blocking}{$idx}{cl}->{TYPE}, $hash->{helper}{blocking}{$idx}{name}, \%message, $item, ++$x, 64, 64); + } + + + if( $hash->{helper}{blocking}{$idx}{cl}->{TYPE} eq 'FHEMWEB' ) { + + $ret .= '
'; + $ret =~ s/&/&/g; + $ret =~ s/'/'/g; + $ret =~ s/\n/
/g; + $ret = "
$ret
" if( $ret =~ m/ / ); + $ret = "$ret"; + + } else { + + #$ret =~ s/]*>//g; + #$ret =~ s/<\/a>//g; + #$ret =~ s/]*>\n//g; + #$ret =~ s/]*>//g; + #$ret =~ s/<\/div>//g; + #$ret =~ s/]*>//g; + #$ret =~ s/<\/h2>//g; + #$ret .= "\n"; + } + + asyncOutput( $hash->{helper}{blocking}{$idx}{cl}, $ret ); + delete $hash->{helper}{blocking}{$idx}; + } + } + } + + return; + } + } + + if( $decode_json->{heos}{command} =~ /^browse\/get_search_criteria/ && ref($decode_json->{payload}) eq "ARRAY" && scalar(@{$decode_json->{payload}}) > 0) { + + push( @{$hash->{helper}{search}{$message{sid}}}, (@{$decode_json->{payload}}) ); + Log3 $name, 3, "HEOSMaster ($name) - call Browser with sid $message{sid}"; + } + + if( $decode_json->{heos}{command} =~ /^browse\/search/ ) { + + Log3 $name, 3, "HEOSMaster ($name) - call search for $message{sid}"; + + if ( defined $message{range} ) { + + $message{range} =~ s/(\d+)\,\d+/$1/; + + } else { + + $message{range} = 0; + $hash->{helper}{searchresult} = []; + } + + my $start = $message{range} + $message{returned}; + push( @{$hash->{helper}{searchresult}}, (@{$decode_json->{payload}}) ); + + if ( $start < $message{count} ) { + + HEOSMaster_Write($hash,"search","sid=$message{sid}&search=$message{search}&scid=$message{scid}",undef); + Log3 $name, 3, "HEOSMaster ($name) - call Search for $message{sid} next Range from $message{returned}"; + + } else { + + if( $hash->{helper}{blocking} && $hash->{helper}{blocking}{cl}{canAsyncOutput} ) { + + my $ret = ''; + + $ret .= sprintf( "%-35s %-10s %s\n", 'Fav', 'type', 'title' ); + #foreach my $item (@{ $hash->{helper}{searchresult}}) { + + # $ret .= HEOSMaster_MakePlayLink($hash->{helper}{blocking}{name}, 'input', $message{sid}, $item, sprintf( "%-35s %-10s %s\n", "x", $item->{type}, $item->{name} ) ); + #} + + $ret .= "\n\n"; + + asyncOutput( $hash->{helper}{blocking}{cl}, $ret ); + delete $hash->{helper}{blocking}; + } + } + + Log3 $name, 4, "HEOSMaster ($name) - call Browser for Search"; + } + + if( $decode_json->{heos}{command} =~ /^player/ or $decode_json->{heos}{command} =~ /^event\/player/ or $decode_json->{heos}{command} =~ /^group/ or $decode_json->{heos}{command} =~ /^event\/group/ or $decode_json->{heos}{command} =~ /^event\/repeat_mode_changed/ or $decode_json->{heos}{command} =~ /^event\/shuffle_mode_changed/ ) { + + if( $decode_json->{heos}{command} =~ /player\/get_players/ ) { + + return Log3 $name, 4, "HEOSMaster ($name) - empty ARRAY received" + unless(scalar(@{$decode_json->{payload}}) > 0); + + my $filter = "TYPE=HEOSPlayer:FILTER=PID!="; + + foreach my $payload (@{$decode_json->{payload}}) { + + $json = '{"pid": "'; + $json .= "$payload->{pid}"; + $json .= '","heos": {"command": "player/get_player_info"}}'; + Dispatch($hash,$json,undef); + Log3 $name, 4, "HEOSMaster ($name) - call Dispatcher for Players"; + $filter .= $payload->{pid}."|"; + } + + chop($filter); #letztes | wieder abschneiden + + #alle Player ausschalten die nicht mehr im HEOS System existieren + foreach my $dev ( devspec2array($filter) ) { + + my $phash = $defs{$dev}; + readingsSingleUpdate( $phash, "state", "off", 1 ); + } + + } elsif( $decode_json->{heos}{command} =~ /group\/get_groups/ ) { + + my $filter = "TYPE=HEOSGroup"; + + if ( scalar(@{$decode_json->{payload}}) > 0 ) { + + $filter .= ":FILTER=GID!="; + + foreach my $payload (@{$decode_json->{payload}}) { + + $json = '{"gid": "'; + $json .= "$payload->{gid}"; + $json .= '","heos": {"command": "group/get_group_info"}}'; + Dispatch($hash,$json,undef); + Log3 $name, 4, "HEOSMaster ($name) - call Dispatcher for Groups"; + $filter .= $payload->{gid}."|"; + } + + chop($filter); #letztes | wieder abschneiden + } + + #alle Gruppe ausschalten die nicht mehr im HEOS System existieren + foreach my $dev ( devspec2array($filter) ) { + + my $ghash = $defs{$dev}; + readingsSingleUpdate( $ghash, "state", "off", 1 ); + } + + } elsif( $decode_json->{heos}{command} =~ /player\/get_player_info/ ) { # ist vielleicht verständlicher? + + Dispatch($hash,$json,undef); + Log3 $name, 4, "HEOSMaster ($name) - call Dispatcher for PlayerInfo"; + + } elsif( $decode_json->{heos}{command} =~ /group\/get_group_info/ ) { # ist vielleicht verständlicher? + + Dispatch($hash,$json,undef); + Log3 $name, 4, "HEOSMaster ($name) - call Dispatcher for GroupInfo"; + + } elsif( $decode_json->{heos}{command} =~ /player\/get_queue/ ) { + + Log3 $name, 3, "HEOSMaster ($name) - call getQueue for player $message{pid}"; + + if ( defined $message{range} ) { + + $message{range} =~ s/(\d+)\,\d+/$1/; + + } else { + + $message{range} = 0; + $hash->{helper}{queue}{$message{pid}} = []; + } + + my $start = $message{range} + $message{returned}; + push( @{$hash->{helper}{queue}{$message{pid}}}, (@{$decode_json->{payload}}) ); + + if ( $start < $message{count} ) { + + HEOSMaster_Write($hash,'getQueue',"pid=$message{pid}&range=$start,".($start + 100),undef); + Log3 $name, 4, "HEOSMaster ($name) - call getQueue for player pid $message{pid} next Range from $start"; + + } else { + + my $code = abs($message{pid}); + $code = $hash->{NAME} ."-". $code if( defined($hash->{NAME}) ); + + if( my $phash = $modules{HEOSPlayer}{defptr}{$code} ) { + + $phash->{helper}{queue} = $hash->{helper}{queue}{$message{pid}}; + delete $hash->{helper}{queue}{$message{pid}}; + } + + $json = '{"heos": {"command": "event/player_queue_changed", "message": "pid='.$message{pid}.'"}}'; + Dispatch($hash,$json,undef); + Log3 $name, 4, "HEOSMaster ($name) - call Dispatcher for Queue Changed"; + } + + } elsif( defined($message{pid}) or defined($message{gid}) ) { + + Dispatch($hash,$json,undef); + Log3 $name, 4, "HEOSMaster ($name) - call Dispatcher"; + } + + return; + } + + Log3 $name, 4, "HEOSMaster ($name) - no Match for processing data"; +} + +sub HEOSMaster_WriteReadings($$) { + + my ($hash,$decode_json) = @_; + my $name = $hash->{NAME}; + + + ############################ + #### Aufbereiten der Daten soweit nötig + my $readingsHash = HEOSMaster_PreProcessingReadings($hash,$decode_json) + if( $decode_json->{heos}{command} eq 'system/register_for_change_events' + or $decode_json->{heos}{command} eq 'system/check_account' + or $decode_json->{heos}{command} eq 'system/sign_in' + or $decode_json->{heos}{command} eq 'system/sign_out' ); + + ############################ + #### schreiben der Readings + + readingsBeginUpdate($hash); + + ### Event Readings + if( ref($readingsHash) eq "HASH" ) { + + Log3 $name, 4, "HEOSMaster ($name) - response json Hash back from HEOSMaster_PreProcessingReadings"; + my $t; + my $v; + + while( ( $t, $v ) = each (%{$readingsHash}) ) { + + readingsBulkUpdate( $hash, $t, $v ) if( defined($v) ); + } + } + + readingsBulkUpdate( $hash, "lastCommand", $decode_json->{heos}{command} ); + readingsBulkUpdate( $hash, "lastResult", $decode_json->{heos}{result} ); + + if( ref($decode_json->{payload}) ne "ARRAY" ) { + + readingsBulkUpdate( $hash, "lastPlayerId", $decode_json->{payload}{pid} ); + readingsBulkUpdate( $hash, "lastPlayerName", $decode_json->{payload}{name} ); + } + + readingsEndUpdate( $hash, 1 ); + return undef; +} + +################### +### my little Helpers + +sub HEOSMaster_ParseMsg($$) { + + my ($hash, $buffer) = @_; + my $name = $hash->{NAME}; + my $open = 0; + my $close = 0; + my $msg = ''; + my $tail = ''; + + + if($buffer) { + foreach my $c (split //, $buffer) { + if($open == $close && $open > 0) { + $tail .= $c; + #Log3 $name, 5, "HEOSMaster ($name) - $open == $close && $open > 0"; + + } elsif(($open == $close) && ($c ne '{')) { + + Log3 $name, 5, "HEOSMaster ($name) - Garbage character before message: " . $c; + + } else { + + if($c eq '{') { + + $open++; + + } elsif($c eq '}') { + + $close++; + } + + $msg .= $c; + } + } + + if($open != $close) { + + $tail = $msg; + $msg = ''; + } + } + + #Log3 $name, 5, "HEOSMaster ($name) - return msg: $msg and tail: $tail"; + return ($msg,$tail); +} + +sub HEOSMaster_PreProcessingReadings($$) { + + my ($hash,$decode_json) = @_; + my $name = $hash->{NAME}; + my $reading; + my %buffer; + my %message = map { my ( $key, $value ) = split "="; $key => $value } split('&', $decode_json->{heos}{message}); + + + Log3 $name, 4, "HEOSMaster ($name) - preprocessing readings"; + + if ( $decode_json->{heos}{command} eq 'system/register_for_change_events' ) { + + $buffer{'enableChangeEvents'} = $message{enable}; + + } elsif ( $decode_json->{heos}{command} eq 'system/check_account' or $decode_json->{heos}{command} eq 'system/sign_in' ) { + if ( exists $message{signed_out} || exists $message{eid} ) { + + $buffer{'heosAccount'} = "signed_out"; + + } else { + + $buffer{'heosAccount'} = "signed_in as $message{un}"; + HEOSMaster_GetFavorites($hash) if( ReadingsVal($name,"enableChangeEvents", "off") eq "on" ); + } + + } else { + + Log3 $name, 3, "HEOSMaster ($name) - no match found"; + return undef; + } + + Log3 $name, 4, "HEOSMaster ($name) - Match found for decode_json"; + return \%buffer; +} + +sub HEOSMaster_FirstRun($) { + + my $hash = shift; + my $name = $hash->{NAME}; + + + RemoveInternalTimer($hash,'HEOSMaster_FirstRun'); + HEOSMaster_Open($hash) if( !IsDisabled($name) ); +} + +sub HEOSMaster_GetPlayers($) { + + my $hash = shift; + my $name = $hash->{NAME}; + + + RemoveInternalTimer($hash,'HEOSMaster_GetPlayers'); + HEOSMaster_Write($hash,'getPlayers',undef,undef); + Log3 $name, 4, "HEOSMaster ($name) - getPlayers"; +} + +sub HEOSMaster_GetGroups($) { + + my $hash = shift; + my $name = $hash->{NAME}; + + + RemoveInternalTimer($hash,'HEOSMaster_GetGroups'); + HEOSMaster_Write($hash,'getGroups',undef,undef); + Log3 $name, 4, "HEOSMaster ($name) - getGroups"; +} + +sub HEOSMaster_EnableChangeEvents($) { + + my $hash = shift; + my $name = $hash->{NAME}; + + + RemoveInternalTimer($hash,'HEOSMaster_EnableChangeEvents'); + HEOSMaster_Write($hash,'enableChangeEvents','on',undef); + Log3 $name, 4, "HEOSMaster ($name) - set enableChangeEvents on"; +} + +sub HEOSMaster_GetMusicSources($) { + + my $hash = shift; + my $name = $hash->{NAME}; + + + RemoveInternalTimer($hash, 'HEOSMaster_GetMusicSources'); + HEOSMaster_Write($hash,'getMusicSources',undef,undef); + Log3 $name, 4, "HEOSMaster ($name) - getMusicSources"; +} + +sub HEOSMaster_GetFavorites($) { + + my $hash = shift; + my $name = $hash->{NAME}; + + + RemoveInternalTimer($hash, 'HEOSMaster_GetFavorites'); + HEOSMaster_Write($hash,'browseSource','sid=1028',undef); + Log3 $name, 4, "HEOSMaster ($name) - getFavorites"; +} + +sub HEOSMaster_GetInputs($) { + + my $hash = shift; + my $name = $hash->{NAME}; + + + RemoveInternalTimer($hash, 'HEOSMaster_GetInputs'); + HEOSMaster_Write($hash,'browseSource','sid=1027',undef); + Log3 $name, 4, "HEOSMaster ($name) - getInputs"; +} + +sub HEOSMaster_GetServers($) { + + my $hash = shift; + my $name = $hash->{NAME}; + + + RemoveInternalTimer($hash, 'HEOSMaster_GetServers'); + HEOSMaster_Write($hash,'browseSource','sid=1024',undef); + Log3 $name, 4, "HEOSMaster ($name) - getServers"; +} + +sub HEOSMaster_GetPlaylists($) { + + my $hash = shift; + my $name = $hash->{NAME}; + + + RemoveInternalTimer($hash, 'HEOSMaster_GetPlaylists'); + HEOSMaster_Write($hash,'browseSource','sid=1025',undef); + Log3 $name, 4, "HEOSMaster ($name) - getPlaylists"; +} + +sub HEOSMaster_GetHistory($) { + + my $hash = shift; + my $name = $hash->{NAME}; + + + RemoveInternalTimer($hash, 'HEOSMaster_GetHistory'); + HEOSMaster_Write($hash,'browseSource','sid=1026',undef); + Log3 $name, 4, "HEOSMaster ($name) - getHistory"; +} + +sub HEOSMaster_CheckAccount($) { + + my $hash = shift; + my $name = $hash->{NAME}; + + + RemoveInternalTimer($hash, 'HEOSMaster_CheckAccount'); + HEOSMaster_Write($hash,'checkAccount',undef,undef); + Log3 $name, 4, "HEOSMaster ($name) - checkAccount"; +} + +sub HEOSMaster_StorePassword($$) { + + my ($hash, $password) = @_; + my $index = $hash->{TYPE}."_".$hash->{NAME}."_passwd"; + my $key = getUniqueId().$index; + my $enc_pwd = ""; + + + if(eval "use Digest::MD5;1") { + + $key = Digest::MD5::md5_hex(unpack "H*", $key); + $key .= Digest::MD5::md5_hex($key); + } + + for my $char (split //, $password) { + + my $encode=chop($key); + $enc_pwd.=sprintf("%.2x",ord($char)^ord($encode)); + $key=$encode.$key; + } + + my $err = setKeyValue($index, $enc_pwd); + return "error while saving the password - $err" if(defined($err)); + + return "password successfully saved"; +} + +sub HEOSMaster_ReadPassword($) { + + my ($hash) = @_; + my $name = $hash->{NAME}; + my $index = $hash->{TYPE}."_".$hash->{NAME}."_passwd"; + my $key = getUniqueId().$index; + my ($password, $err); + + + Log3 $name, 4, "HEOSMaster ($name) - Read password from file"; + + ($err, $password) = getKeyValue($index); + + if ( defined($err) ) { + + Log3 $name, 4, "HEOSMaster ($name) - unable to read password from file: $err"; + return undef; + + } + + if ( defined($password) ) { + if ( eval "use Digest::MD5;1" ) { + + $key = Digest::MD5::md5_hex(unpack "H*", $key); + $key .= Digest::MD5::md5_hex($key); + } + + my $dec_pwd = ''; + + for my $char (map { pack('C', hex($_)) } ($password =~ /(..)/g)) { + + my $decode=chop($key); + $dec_pwd.=chr(ord($char)^ord($decode)); + $key=$decode.$key; + } + + return $dec_pwd; + + } else { + + Log3 $name, 4, "HEOSMaster ($name) - No password in file"; + return undef; + } +} + +sub HEOSMaster_MakePlayLink3($$$$) { + + my ($name, $message, $item, $idx) = @_; + my $xcmd; + my $xtext = $message->{sid}; + + + if ( (exists $item->{playable} && $item->{playable} eq "yes") || exists $item->{qid} ) { + + $xcmd = 'cmd'.uri_escape('=set '.$name.' input '.$message->{sid}); + + } else { + + $xcmd = 'cmd'.uri_escape('=get '.$name.' ls '.$message->{sid}); + } + + if ( defined $item->{sid} ) { + + $xcmd = 'cmd'.uri_escape('=get '.$name.' ls '.$message->{sid}); + $xcmd .= uri_escape(",".$item->{sid}); + $xtext .= ','.$item->{sid}; + + } elsif ( defined $item->{cid} ) { + if ( $item->{type} eq "album" ) { + + $xcmd = 'cmd'.uri_escape('=get '.$name.' ls '.$message->{sid}); + } + + $xcmd .= uri_escape(",".$item->{cid}); + $xtext .= ','.$item->{cid}; + + } elsif ( defined $item->{mid} ) { + if ( $message->{sid} eq "1028" ) { + + $xcmd .= ','.$idx; + $xtext .= ','.$idx; + + } elsif ( defined $message->{cid} ) { + + $xcmd .= uri_escape(','.$message->{cid}.','.$item->{mid}); + $xtext .= ','.$message->{cid}.','.$item->{mid}; + + } else { + + $xcmd = 'cmd'.uri_escape('=set '.$name.' input 1027'); + $xcmd .= uri_escape(','.$message->{sid}.','.$item->{mid}); + $xtext = '1027,'.$message->{sid}.','.$item->{mid}; + } + } elsif ( defined $item->{qid} ) { + + $xcmd .= ','.$item->{qid}; + $xtext .= ','.$item->{qid}; + } + + $xcmd = "FW_cmd('$FW_ME$FW_subdir?XHR=1&$xcmd')"; + return '
  • '.sprintf( "%-35s %-15s %s", $xtext, $item->{type}, $item->{name} )."
  • \n"; +} + +sub HEOSMaster_MakePlayLink($$$$$$$) { + + my ($type, $name, $message, $item, $idx, $xsize, $ysize) = @_; + my $xcmd; + my $xtext = $message->{sid}; + + $ysize = '10.75em' if (!defined($ysize)); + + if ( (exists $item->{playable} && $item->{playable} eq "yes") || exists $item->{qid} ) { + + $xcmd = 'cmd'.uri_escape('=set '.$name.' input '.$message->{sid}); + $xtext = '*'.$xtext; + + } else { + + $xcmd = 'cmd'.uri_escape('=get '.$name.' ls '.$message->{sid}); + } + + if ( defined $item->{sid} ) { + + $xcmd = 'cmd'.uri_escape('=get '.$name.' ls '.$message->{sid}); + $xcmd .= uri_escape(",".$item->{sid}); + $xtext .= ','.$item->{sid}; + + } elsif ( defined $item->{cid} ) { + if ( $item->{type} eq "album" ) { + + $xcmd = 'cmd'.uri_escape('=get '.$name.' ls '.$message->{sid}); + } + + $xcmd .= uri_escape(",".$item->{cid}); + $xtext .= ','.$item->{cid}; + + } elsif ( defined $item->{mid} ) { + if ( $message->{sid} eq "1028" ) { + + $xcmd .= ','.$idx; + $xtext .= ','.$idx; + + } elsif ( defined $message->{cid} ) { + + $xcmd .= uri_escape(','.$message->{cid}.','.$item->{mid}); + $xtext .= ','.$message->{cid}.','.$item->{mid}; + + } else { + + $xcmd = 'cmd'.uri_escape('=set '.$name.' input 1027'); + $xcmd .= uri_escape(','.$message->{sid}.','.$item->{mid}); + $xtext = '1027,'.$message->{sid}.','.$item->{mid}; + } + } elsif ( defined $item->{qid} ) { + + $xcmd .= ','.$item->{qid}; + $xtext .= ','.$item->{qid}; + } + + if( $type eq 'FHEMWEB' ) { + + $xcmd = "FW_cmd('$FW_ME$FW_subdir?XHR=1&$xcmd')"; + + if ( defined $item->{image_url} && $item->{image_url} ne "" ) { + return '
    '.$item->{name}.'
    '; + + #return '
    '.$item->{name}."
    \n\n"; + } else { + return '

    '.$item->{name}.'

    '; + #return '\n\n"; + } + + } else { + + return sprintf( "%-15s %s\n", $xtext, $item->{name} ); + + } +} + +sub HEOSMaster_MakeImage($$) { + + my ($url, $size) = @_; + my $ret .= "\n"; + + return $ret; +} + + + + + + + + +1; + + + + +=pod +=item device +=item summary Modul to controls the Denon multiroom soundsystem +=item summary_DE Modul zum steuern des Denon Multiroom-Soundsystem + +=begin html + + +

    HEOSMaster

    +
      + HEOSMaster +

      + In combination with HEOSPlayer and HEOSGroup this FHEM Module controls the Denon multiroom soundsystem using a telnet socket connection and the HEOS Command Line Interface (CLI). +

      + Prerequisite +
        +
      • Installation of the following packages: apt-get install libjson-perl libnet-telnet-perl libencode-perl +
      • +
      +
      + + Define +

        + define <name> HEOSMaster <IP address> +

        + Example: +

          + define MyMasterBox HEOSMaster 192.168.178.67
          +
        +
        + <IP address> is the IP address of Your HEOS receiver or HEOS box. The master device is created in the room HEOS, then the players of Your system are recognized automatically and created in FHEM. From now on the players can be controlled and changes in the HEOS app or at the Receiver are synchronized with the state and media readings of the players. + +

        + Readings +
          +
        • enableChangeEvents - state of the event reproduction at CLI master (on|off)
        • +
        • heosAccount - signed_out | signed_in as <HEOSAccount>
        • +
        • lastCommand - last executed command
        • +
        • lastPlayerId - player id of the device, which executed the last command
        • +
        • lastPlayerName - player name of the device, which executed the last command
        • +
        • lastResult - result of the last executed command
        • +
        • state - state of the HEOSMaster
        • +
        +

        + + set +
          +
        • checkAccount - checks Your HEOS account
        • +
        • enableChangeEvents - activates the event reproduction at the CLI master
        • +
        • getGroups - get a list of all groups and creates the devices, if not done already
        • +
        • getPlayers - get a list of all players and creates the devices, if not yet existing
        • +
        • password - set the password of Your HEOS account
        • +
        • reboot - reboot of the CLI interface at HEOSMaster
        • +
        • reopen - tries to establish a new socket connection with CLI master
        • +
        • signAccount In|Out - sign in|out Your HEOS account (attr MyMasterBox heosUsername <username>)
        • +
        +

        + + get +
          +
        • ShowAccount - shows Your HEOS account
        • +
        +

        + + state +
          +
        • connected - the HEOSmaster is connected to the CLI Master
        • +
        • not connected - the HEOSmaster is not connected to the CLI Master
        • +
        +

        + + attributes +
          +
        • heosUsername - username of Your HEOS account
        • +
        +

        +
      +
    + +=end html + +=begin html_DE + + +

    HEOSMaster

    +
      + HEOSMaster +

      + In Kombination mit HEOSPlayer und HEOSGroup steuert dieses FHEM Modul das Denon Multiroom-Soundsystem mit Hilfe einer telnet Socket-Verbindung und dem HEOS Command Line Interface (CLI). +

      + Voraussetzung +
        +
      • Installation der folgenden Pakete: apt-get install libjson-perl libnet-telnet-perl libencode-perl +
      • +
      +
      + + Define +

        + define <name> HEOSMaster <IP address> +

        + Example: +

          + define MyMasterBox HEOSMaster 192.168.178.67
          +
        +
        + <IP address> ist die IP-Adresse des HEOS Receivers oder der HEOS Box. Das Master Device wird im Raum HEOS angelegt und danach erfolgt das Einlesen und automatische Anlegen der Player. +Von nun an können die Player gesteuert werden. Außerdem wird der Status und die Media Readings der Player entsprechend geändert, wenn man in der HEOS-App oder direkt am Receiver etwas ändert. + + +

        + Readings +
          +
        • enableChangeEvents - Status der Event Wiedergabe auf dem CLI Master
        • +
        • heosAccount - signed_out | signed_in as <HEOSAccount>
        • +
        • lastCommand - zuletzt ausgeführtes Kommando
        • +
        • lastPlayerId - Player-Id des Geräts, welches das Kommando ausgeführt hat
        • +
        • lastPlayerName - Player-Name des Geräts, welches das Kommando ausgeführt hat
        • +
        • lastResult - Ergebnis des zuletzt ausgeführten Kommandos
        • +
        • state - Status des HEOSMaster
        • +
        +

        + + set +
          +
        • checkAccount - prüft das HEOS Konto
        • +
        • enableChangeEvents - aktiviert die Event Wiedergabe auf dem CLI Master
        • +
        • getGroups - holt eine Liste aller Gruppen und legt die Devices an, sofern noch nicht geschehen
        • +
        • getPlayers - holt eine Liste aller Player und legt die Devices an, sofern noch nicht vorhanden
        • +
        • password - setzt das Passwort des HEOS Kontos
        • +
        • reboot - rebootet das CLI Interface am Master
        • +
        • reopen - versucht eine neue Socket-Verbindung zum CLI Master aufzubauen
        • +
        • signAccount In|Out - anmelden|abmelden am HEOS Konto (attr MyMasterBox heosUsername <username>)
        • +
        +

        + + get +
          +
        • ShowAccount - zeigt das HEOS Konto an
        • +
        +

        + + state +
          +
        • connected - der HEOSmaster ist mit dem CLI Master verbunden
        • +
        • not connected - der HEOSmaster ist nicht mit dem CLI Master verbunden
        • +
        +

        + + Attributes +
          +
        • heosUsername - Benutzername des HEOS Kontos
        • +
        +

        +
      +
    + +=end html_DE + +=cut diff --git a/fhem/FHEM/21_HEOSPlayer.pm b/fhem/FHEM/21_HEOSPlayer.pm new file mode 100644 index 000000000..ad88b5d7b --- /dev/null +++ b/fhem/FHEM/21_HEOSPlayer.pm @@ -0,0 +1,1292 @@ +############################################################################### +# +# Developed with Kate +# +# (c) 2017 Copyright: Marko Oldenburg (leongaultier at gmail dot com) +# All rights reserved +# +# Special thanks goes to comitters: +# - Olaf Schnicke Thanks for many many Code +# - Dieter Hehlgans Thanks for 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$ +# +############################################################################### + +package main; + +use strict; +use warnings; +use JSON qw(decode_json); +use Encode qw(encode_utf8); +use URI::Escape; +#use Data::Dumper; + +my $version = "0.2.0"; + + + + +# Declare functions +sub HEOSPlayer_Initialize($); +sub HEOSPlayer_Define($$); +sub HEOSPlayer_Undef($$); +sub HEOSPlayer_Attr(@); +sub HEOSPlayer_Parse($$); +sub HEOSPlayer_WriteReadings($$); +sub HEOSPlayer_Set($$@); +sub HEOSPlayer_PreProcessingReadings($$); +sub HEOSPlayer_GetPlayerInfo($); +sub HEOSPlayer_GetPlayState($); +sub HEOSPlayer_GetQueue($); +sub HEOSPlayer_GetNowPlayingMedia($); +sub HEOSPlayer_GetPlayMode($); +sub HEOSPlayer_GetVolume($); +sub HEOSPlayer_Get($$@); +sub HEOSPlayer_GetMute($); +sub HEOSPlayer_MakePlayLink($$$$$$$$); + + + +sub HEOSPlayer_Initialize($) { + + my ($hash) = @_; + + $hash->{Match} = '.*{"command":."player.*|.*{"command":."event/player.*|.*{"command":."event\/repeat_mode_changed.*|.*{"command":."event\/shuffle_mode_changed.*|.*{"command":."event\/favorites_changed.*'; + + + # Provider + $hash->{SetFn} = "HEOSPlayer_Set"; + $hash->{GetFn} = "HEOSPlayer_Get"; + $hash->{DefFn} = "HEOSPlayer_Define"; + $hash->{UndefFn} = "HEOSPlayer_Undef"; + $hash->{AttrFn} = "HEOSPlayer_Attr"; + $hash->{ParseFn} = "HEOSPlayer_Parse"; + $hash->{AttrList} = "IODev ". + "disable:1 ". + "mute2play:1 ". + "channelring:1 ". + $readingFnAttributes; + + foreach my $d(sort keys %{$modules{HEOSPlayer}{defptr}}) { + + my $hash = $modules{HEOSPlayer}{defptr}{$d}; + $hash->{VERSION} = $version; + } +} + +sub HEOSPlayer_Define($$) { + + my ( $hash, $def ) = @_; + my @a = split( "[ \t]+", $def ); + splice( @a, 1, 1 ); + my $iodev; + my $i = 0; + + + foreach my $param ( @a ) { + if( $param =~ m/IODev=([^\s]*)/ ) { + + $iodev = $1; + splice( @a, $i, 3 ); + last; + } + + $i++; + } + + return "too few parameters: define HEOSPlayer " if( @a < 2 ); + + my ($name,$pid) = @a; + + $hash->{PID} = $pid; + $hash->{VERSION} = $version; + AssignIoPort($hash,$iodev) if( !$hash->{IODev} ); + + if(defined($hash->{IODev}->{NAME})) { + + Log3 $name, 3, "HEOSPlayer ($name) - I/O device is " . $hash->{IODev}->{NAME}; + + } else { + + Log3 $name, 1, "HEOSPlayer ($name) - no I/O device"; + } + + $iodev = $hash->{IODev}->{NAME}; + my $code = abs($pid); + + $code = $iodev."-".$code if( defined($iodev) ); + my $d = $modules{HEOSPlayer}{defptr}{$code}; + + return "HEOSPlayer device $hash->{pid} on HEOSMaster $iodev already defined as $d->{NAME}." + if( defined($d) && $d->{IODev} == $hash->{IODev} && $d->{NAME} ne $name ); + + Log3 $name, 3, "HEOSPlayer ($name) - defined with Code: $code"; + $attr{$name}{room} = "HEOS" if( !defined( $attr{$name}{room} ) ); + $attr{$name}{devStateIcon} = "on:10px-kreis-gruen off:10px-kreis-rot" if( !defined( $attr{$name}{devStateIcon} ) ); + + if( $init_done ) { + + InternalTimer( gettimeofday()+int(rand(2)), "HEOSPlayer_GetPlayerInfo", $hash, 0 ); + InternalTimer( gettimeofday()+int(rand(4)), "HEOSPlayer_GetPlayState", $hash, 0 ); + InternalTimer( gettimeofday()+int(rand(6)), "HEOSPlayer_GetNowPlayingMedia", $hash, 0 ); + InternalTimer( gettimeofday()+int(rand(8)), "HEOSPlayer_GetPlayMode", $hash, 0 ); + InternalTimer( gettimeofday()+int(rand(10)), "HEOSPlayer_GetVolume", $hash, 0 ); + InternalTimer( gettimeofday()+int(rand(12)), "HEOSPlayer_GetMute", $hash, 0 ); + InternalTimer( gettimeofday()+int(rand(14)), "HEOSPlayer_GetQueue", $hash, 0 ); + + } else { + + InternalTimer( gettimeofday()+15+int(rand(2)), "HEOSPlayer_GetPlayerInfo", $hash, 0 ); + InternalTimer( gettimeofday()+15+int(rand(4)), "HEOSPlayer_GetPlayState", $hash, 0 ); + InternalTimer( gettimeofday()+15+int(rand(6)), "HEOSPlayer_GetNowPlayingMedia", $hash, 0 ); + InternalTimer( gettimeofday()+15+int(rand(8)), "HEOSPlayer_GetPlayMode", $hash, 0 ); + InternalTimer( gettimeofday()+15+int(rand(10)), "HEOSPlayer_GetVolume", $hash, 0 ); + InternalTimer( gettimeofday()+15+int(rand(12)), "HEOSPlayer_GetMute", $hash, 0 ); + InternalTimer( gettimeofday()+15+int(rand(14)), "HEOSPlayer_GetQueue", $hash, 0 ); + } + + readingsBeginUpdate($hash); + readingsBulkUpdate($hash, 'state','Initialized'); + readingsBulkUpdate($hash, 'volumeUp', 5); + readingsBulkUpdate($hash, 'volumeDown', 5); + readingsEndUpdate($hash, 1); + + $modules{HEOSPlayer}{defptr}{$code} = $hash; + return undef; +} + +sub HEOSPlayer_Undef($$) { + + my ( $hash, $arg ) = @_; + my $pid = $hash->{PID}; + my $name = $hash->{NAME}; + + + RemoveInternalTimer($hash); + my $code = abs($pid); + $code = $hash->{IODev}->{NAME} ."-". $code if( defined($hash->{IODev}->{NAME}) ); + delete($modules{HEOSPlayer}{defptr}{$code}); + + Log3 $name, 3, "HEOSPlayer ($name) - device $name deleted with Code: $code"; + return undef; +} + +sub HEOSPlayer_Attr(@) { + + my ( $cmd, $name, $attrName, $attrVal ) = @_; + my $hash = $defs{$name}; + my $token = $hash->{IODev}->{TOKEN}; + + + if( $attrName eq "disable" ) { + if( $cmd eq "set" and $attrVal eq "1" ) { + + readingsSingleUpdate ( $hash, "state", "disabled", 1 ); + Log3 $name, 3, "HEOSPlayer ($name) - disabled"; + + } elsif( $cmd eq "del" ) { + + readingsSingleUpdate ( $hash, "state", "active", 1 ); + Log3 $name, 3, "HEOSPlayer ($name) - enabled"; + } + } + + if( $attrName eq "disabledForIntervals" ) { + if( $cmd eq "set" ) { + + Log3 $name, 3, "HEOSPlayer ($name) - enable disabledForIntervals"; + readingsSingleUpdate ( $hash, "state", "Unknown", 1 ); + + } elsif( $cmd eq "del" ) { + + readingsSingleUpdate ( $hash, "state", "active", 1 ); + Log3 $name, 3, "HEOSPlayer ($name) - delete disabledForIntervals"; + } + } +} + +sub HEOSPlayer_Get($$@) { + + my ($hash, $name, @aa) = @_; + my ($cmd, @args) = @aa; + my $pid = $hash->{PID}; + my $result = ""; + my $me = {}; + my $ret; + + $me->{cl} = $hash->{CL} if( ref($hash->{CL}) eq 'HASH' ); + $me->{name} = $hash->{NAME}; + $me->{pid} = $hash->{PID}; + + #Leerzeichen müßen für die Rückgabe escaped werden sonst werden sie falsch angezeigt + if( $cmd eq 'channelscount' ) { + + #gibt die Favoritenanzahl zurück + return scalar(@{$hash->{IODev}{helper}{favorites}}) if ( defined $hash->{IODev}{helper}{favorites} ); + + } elsif( $cmd eq 'ls' ) { + + my $param = shift( @args ); + $param = '' if( !$param ); + #$param = substr($param,1) if( $param && $param =~ '^|' ); + + if ( $param eq '' ) { + + if( $me->{cl}->{TYPE} eq 'FHEMWEB' ) { + + $ret = '
    '; + $ret .= '

    Musik


    '; + $ret .= '
    '; + + } else { + + $ret = "Musik\n"; + $ret .= sprintf( "%-15s %s\n", 'key', 'title' ); + + } + + foreach my $item (@{ $hash->{IODev}{helper}{sources}}) { + $ret .= HEOSPlayer_MakePlayLink($me->{cl}->{TYPE}, $hash->{NAME}, $item->{sid}, $item->{type}, $item->{name}, $item->{image_url}, 128, 50); + } + + $ret .= HEOSPlayer_MakePlayLink($me->{cl}->{TYPE}, $hash->{NAME}, "1025", "heos_service", "Playlist", "https://production.ws.skyegloup.com:443/media/images/service/logos/musicsource_logo_playlists.png", 128, 32); + + $ret .= HEOSPlayer_MakePlayLink($me->{cl}->{TYPE}, $hash->{NAME}, "1026", "heos_service", "Verlauf", "https://production.ws.skyegloup.com:443/media/images/service/logos/musicsource_logo_history.png", 128, 32); + + $ret .= HEOSPlayer_MakePlayLink($me->{cl}->{TYPE}, $hash->{NAME}, "1027", "heos_service", "Eingänge", "https://production.ws.skyegloup.com:443/media/images/service/logos/musicsource_logo_aux.png", 128, 32); + + $ret .= HEOSPlayer_MakePlayLink($me->{cl}->{TYPE}, $hash->{NAME}, "1028", "heos_service", "Favoriten", "https://production.ws.skyegloup.com:443/media/images/service/logos/musicsource_logo_favorites.png", 128, 32); + + $ret .= HEOSPlayer_MakePlayLink($me->{cl}->{TYPE}, $hash->{NAME}, "1029", "heos_service", "Warteschlange", "https://production.ws.skyegloup.com:443/media/images/service/logos/musicsource_logo_playlists.png", 128, 32); + + #$ret .= "\n\n"; + + if( $me->{cl}->{TYPE} eq 'FHEMWEB' ) { + + $ret .= '
    '; + $ret =~ s/&/&/g; + $ret =~ s/'/'/g; + $ret =~ s/\n/
    /g; + $ret = "
    $ret
    " if( $ret =~ m/ / ); + $ret = "$ret"; + + } else { + + #$ret =~ s/]*>//g; + #$ret =~ s/<\/h3>/\n\n/g; + #$ret =~ s/]*>//g; + #$ret =~ s/<\/h5>/\n/g; + #$ret =~ s/
    //g; + #$ret =~ s/]*>//g; + #$ret =~ s/<\/a>//g; + #$ret =~ s/]*>//g; + #$ret =~ s/]*>//g; + #$ret =~ s/<\/div>//g; + #$ret .= "\n"; + + } + + return $ret; + + } else { + + my ($sid,$cid) = split /,/,$param; + #$param=~/^(-?\d+),?(.*)$/; + + if ( $sid eq "1025" ) { + + $me->{sourcename} = "Playlist"; + + } elsif ( $sid eq "1026" ) { + + $me->{sourcename} = "Verlauf"; + + } elsif ( $sid eq "1027" ) { + + $me->{sourcename} = "Eingänge"; + + } elsif ( $sid eq "1028" ) { + + $me->{sourcename} = "Favoriten"; + + } elsif ( $sid eq "1029" ) { + + $me->{sourcename} = "Warteschlange"; + + } else { + + my @sids = map { $_->{name} } grep { $_->{sid} =~ /$sid/i } (@{ $hash->{IODev}{helper}{sources} }); + $me->{sourcename} = $sids[0] if ( scalar @sids > 0); + } + + my $heosCmd = "browseSource"; + my $action; + + if ( defined $sid && defined $cid && $cid ne "" ) { + if ( $sid eq "1027" ) { + + $action = "sid=$cid"; + + } elsif ( $sid eq "1026" ) { + + $me->{sourcename} .= "/$cid"; + $action = "sid=$sid&cid=$cid"; + + } else { + + my @cids = map { $_->{name} } grep { $_->{cid} =~ /\Q$cid\E/i } (@{ $hash->{IODev}{helper}{media} }); + $me->{sourcename} .= "/".$cids[0] if ( scalar @cids > 0); + $action = "sid=$sid&cid=$cid"; + + } + + } elsif ( defined $sid && $sid eq "1029" ) { + + if( $me->{cl}->{TYPE} eq 'FHEMWEB' ) { + + $ret = '
    '; + $ret .= '

    '.$hash->{NAME}.'

    '; + $ret .= '

    Warteschlange


    '; + + $ret .= ''; + + $ret .= '
    '; + + } else { + + $ret .= "Warteschlange von $hash->{NAME} \n"; + $ret .= sprintf( "%-15s %s\n", 'key', 'title' ); + } + + my $x = 0; + my $itemtext; + my $itemkey; + + foreach my $item (@{ $hash->{helper}{queue}}) { + + $itemtext = $item->{artist}; + $itemtext .= ( defined $item->{artist} ) ? (", ".$item->{album}) : ($item->{album}) if ( defined $item->{album} ); + $itemkey = '1029,'.++$x; + + if( $me->{cl}->{TYPE} eq 'FHEMWEB' ) { + my $xcmd = 'cmd'.uri_escape('=set '.$hash->{NAME}.' input '.$itemkey); + $xcmd = "FW_cmd('$FW_ME$FW_subdir?XHR=1&$xcmd')"; + $ret .= '

    '.$item->{song}.'

    '.$itemtext."

    "; + #$ret .= HEOSPlayer_MakePlayLink($hash->{NAME}, "1029,".++$x, "heos_queue", $item->{song}, $item->{image_url}, 64, 64); + } else { + $ret .= sprintf( "%-15s %s\n", $itemkey, $item->{song}.", ".$itemtext ) + } + } + + if( $me->{cl}->{TYPE} eq 'FHEMWEB' ) { + + $ret .= '
    '; + $ret =~ s/&/&/g; + $ret =~ s/'/'/g; + $ret =~ s/\n/
    /g; + $ret = "
    $ret
    " if( $ret =~ m/ / ); + $ret = "$ret"; + + } else { + + #$ret =~ s/]*>//g; + #$ret =~ s/<\/a>//g; + #$ret =~ s/]*>\n//g; + #$ret =~ s/]*>//g; + #$ret =~ s/<\/div>//g; + #$ret .= "\n"; + } + + asyncOutput( $me->{cl}, $ret ); + + } else { + + $action = "sid=$sid"; + } + + IOWrite($hash,$heosCmd,$action,$me); + Log3 $name, 4, "HEOSPlayer ($name) - IOWrite: $heosCmd $action IODevHash=$hash->{IODev}"; + + return undef; + } + } + + my $list = 'channelscount:noArg ls'; + + return "Unknown argument $cmd, choose one of $list"; +} + +sub HEOSPlayer_Set($$@) { + + my ($hash, $name, @aa) = @_; + my ($cmd, @args) = @aa; + my $pid = $hash->{PID}; + my $action; + my $heosCmd; + my $rvalue; + my $favoritcount = 1; + my $qcount = 1; + my $string = "pid=$pid"; + + return undef unless ( ReadingsVal($name, "state", "off") eq "on" ); + + if( $cmd eq 'getPlayerInfo' ) { + return "usage: $cmd" if( @args != 0 ); + + $heosCmd = $cmd; + + } elsif( $cmd eq 'getPlayState' ) { + return "usage: $cmd" if( @args != 0 ); + + $heosCmd = $cmd; + + } elsif( $cmd eq 'getPlayMode' ) { + return "usage: $cmd" if( @args != 0 ); + + $heosCmd = $cmd; + + } elsif( $cmd eq 'getNowPlayingMedia' ) { + return "usage: $cmd" if( @args != 0 ); + + $heosCmd = $cmd; + + } elsif( $cmd eq 'repeat' ) { + my $param = "one|all|off"; + return "usage: $cmd $param" if( @args != 1 || ! grep { $_ =~ /$args[0]/ } split(/\|/, $param) ); + + $heosCmd = 'setPlayMode'; + $rvalue = 'on_'.$args[0]; + $rvalue = 'off' if($rvalue eq 'on_off'); + $action = "repeat=$rvalue&shuffle=".ReadingsVal($name,'shuffle','off'); + + } elsif( $cmd eq 'shuffle' ) { + my $param = "on|off"; + return "usage: $cmd $param" if( @args != 1 || ! grep { $_ =~ /$args[0]/ } split(/\|/, $param) ); + + $heosCmd = 'setPlayMode'; + $rvalue = 'on_'.ReadingsVal($name,'repeat','off'); + $rvalue = 'off' if($rvalue eq 'on_off'); + $action = "repeat=$rvalue&shuffle=$args[0]"; + + } elsif( $cmd eq 'play' ) { + return "usage: $cmd" if( @args != 0 ); + + $heosCmd = 'setPlayState'; + $action = "state=$cmd"; + + } elsif( $cmd eq 'stop' ) { + return "usage: $cmd" if( @args != 0 ); + + $heosCmd = 'setPlayState'; + $action = "state=$cmd"; + + } elsif( $cmd eq 'pause' ) { + return "usage: $cmd" if( @args != 0 ); + + $heosCmd = 'setPlayState'; + $action = "state=$cmd"; + + } elsif( $cmd eq 'mute' ) { + my $param = "on|off"; + return "usage: $cmd $param" if( @args != 1 || ! grep { $_ =~ /$args[0]/ } split(/\|/, $param) ); + + $heosCmd = 'setMute'; + $action = "state=$args[0]"; + + } elsif( $cmd eq 'volume' ) { + return "usage: $cmd 0-100" if( @args != 1 || $args[0] !~ /(\d+)/ || $args[0] > 100 || $args[0] < 0); + + $heosCmd = 'setVolume'; + $action = "level=$args[0]"; + + } elsif( $cmd eq 'volumeUp' ) { + return "usage: $cmd 0-10" if( @args != 1 || $args[0] !~ /(\d+)/ || $args[0] > 10 || $args[0] < 1); + + $heosCmd = $cmd; + $action = "step=$args[0]"; + + } elsif( $cmd eq 'volumeDown' ) { + return "usage: $cmd 0-10" if( @args != 1 || $args[0] !~ /(\d+)/ || $args[0] > 10 || $args[0] < 1); + + $heosCmd = $cmd; + $action = "step=$args[0]"; + + } elsif( $cmd eq 'groupWithMember' ) { + return "usage: $cmd" if( @args != 1 ); + + foreach ( split('\,', $args[0]) ) { + + $string .= ",$defs{$_}->{PID}" if ( defined $defs{$_} ); + printf "String: $string\n"; + } + + $heosCmd = 'createGroup'; + + } elsif( $cmd eq 'groupClear' ) { + return "usage: $cmd" if( @args != 0 ); + + $heosCmd = 'createGroup'; + + } elsif( $cmd eq 'next' ) { + return "usage: $cmd" if( @args != 0 ); + + $heosCmd = 'playNext'; + + } elsif( $cmd eq 'prev' ) { + return "usage: $cmd" if( @args != 0 ); + + $heosCmd = 'playPrev'; + + } elsif ( $cmd =~ /channel/ ) { + + my $favorit = ReadingsVal($name,"channel", 1); + + $favoritcount = scalar(@{$hash->{IODev}{helper}{favorites}}) if ( defined $hash->{IODev}{helper}{favorites} ); + $heosCmd = 'playPresetStation'; + + if ( $cmd eq 'channel' ) { + return "usage: channel 1-$favoritcount" if( @args != 1 || $args[0] !~ /(\d+)/ || $args[0] > $favoritcount || $args[0] < 1); + + $action = "preset=$args[0]"; + + } elsif( $cmd eq 'channelUp' ) { + return "usage: $cmd" if( @args != 0 ); + + ++$favorit; + if ( $favorit > $favoritcount ) { + if ( AttrVal($name, 'channelring', 0) == 1 ) { + + $favorit = 1; + + } else { + + $favorit = $favoritcount; + } + } + + $action = "preset=".$favorit; + + } elsif( $cmd eq 'channelDown' ) { + return "usage: $cmd" if( @args != 0 ); + + --$favorit; + if ( $favorit <= 0 ) { + if ( AttrVal($name, 'channelring', 0) == 1 ) { + + $favorit = $favoritcount; + + } else { + + $favorit = 1; + } + } + + $action = "preset=".$favorit; + } + + } elsif ( $cmd =~ /Queue/ ) { + + $heosCmd = $cmd; + if ( $cmd eq 'playQueueItem' ) { + + $qcount = scalar(@{$hash->{helper}{queue}}) if ( defined $hash->{helper}{queue} ); + return "usage: queue 1-$qcount" if( @args != 1 || $args[0] !~ /(\d+)/ || $args[0] > $qcount || $args[0] < 1); + + $action = "qid=$args[0]"; + + } elsif ( $cmd eq 'clearQueue' ) { + #löscht die Warteschlange + return "usage: $cmd" if( @args != 0 ); + + delete $hash->{helper}{queue}; + + } elsif ( $cmd eq 'saveQueue' ) { + + #speichert die aktuelle Warteschlange als Playlist ab + return "usage: saveQueue" if( @args != 1 ); + + $action = "name=$args[0]"; + } + + } elsif ( $cmd =~ /Playlist/ ) { + + my $mid; + my $cid = $args[0]; + my @path = split(",", $args[0]) if ( @args != 0 && $args[0] =~ /,/ ); + $cid = $path[0] if ( scalar @path > 0); + $mid = $path[1] if ( scalar @path > 1); + + if ( scalar @args != 0 ) { + + if ( $cid !~ /^-*[0-9]+$/ ) { + + my @cids = map { $_->{cid} } grep { $_->{name} =~ /\Q$cid\E/i } (@{ $hash->{IODev}{helper}{playlists} }); + return "usage: $cmd name" if ( scalar @cids <= 0); + + $cid = $cids[0]; + } + + if ( $cmd eq 'playPlaylist' ) { + + $heosCmd = $cmd; + $action = "sid=1025&cid=$cid&aid=4"; + + } elsif ( $cmd eq 'playPlaylistItem' ) { + return "usage: playPlaylistItem name,nr" if ( scalar @path < 2); + + $heosCmd = 'playPlaylist'; + $action = "sid=1025&cid=$cid&mid=$mid&aid=4"; + + } elsif ( $cmd eq 'deletePlaylist' ) { + + $heosCmd = $cmd; + $action = "cid=$cid"; + $string = "sid=1025"; + } + + } else { + + my @playlists = map { $_->{name} } (@{ $hash->{IODev}{helper}{playlists}}); + return "usage: $cmd name|id".join(",",@playlists); + } + + } elsif( $cmd eq 'aux' ) { + return "usage: $cmd" if( @args != 0 ); + + my $auxname = @{ $hash->{helper}{aux} }[0]->{mid}; + $heosCmd = 'playInput'; + $action = "input=$auxname"; + + Log3 $name, 4, "HEOSPlayer ($name) - set aux to $auxname"; + readingsSingleUpdate($hash, "input", $args[0], 1); + + } elsif( $cmd eq 'input' ) { + return "usage: $cmd sid[,cid][,mid]" if( @args != 1 ); + + my $param = shift( @args ); + my ($sid,$cid,$mid) = split /,/,$param; + return "usage: $cmd sid[,cid][,mid]" unless( defined $sid || $sid eq "" ); + + if ( $sid eq "1024" ) { + return "usage: $cmd sid,cid[,mid]" unless( defined($cid) && defined($mid) ); + + #Server abspielen + $heosCmd = 'playPlaylist'; + $action = "sid=$sid&cid=$cid&aid=4"; + $action = "sid=$sid&cid=$cid&mid=$mid&aid=4" if ( defined($mid) ); + + } elsif ( $sid eq "1025" ) { + return "usage: $cmd sid,cid[,mid]" unless( defined($cid) ); + + #Playlist abspielen + $heosCmd = 'playPlaylist'; + $action = "sid=$sid&cid=$cid&aid=4"; + $action = "sid=$sid&cid=$cid&mid=$mid&aid=4" if ( defined($mid) ); + + } elsif ( $sid eq "1026" ) { + return "usage: $cmd sid,cid,mid" unless( defined($cid) ); + + #Verlauf abspielen + if ( $cid eq "TRACKS" ) { + + $heosCmd = 'playPlaylist'; + $action = "sid=$sid&cid=$cid&aid=4"; + $action = "sid=$sid&cid=$cid&mid=$mid&aid=4" if ( defined($mid) ); + + } elsif ( $cid eq "STATIONS" ) { + + $heosCmd = 'playStream'; + $action = "sid=$sid&cid=$cid&mid=$mid"; + } + + } elsif ( $sid eq "1027" ) { + return "usage: $cmd sid,spid,mid" unless( defined($cid) ); + + #Eingang abspielen + $heosCmd = 'playInput'; + $action = "input=$mid"; + $action = "spid=$cid&".$action if ( $pid ne $cid ); + + } elsif ( $sid eq "1028" ) { + return "usage: $cmd sid,nr" unless( defined($cid) ); + + #Favoriten abspielen + $heosCmd = 'playPresetStation'; + $action = "preset=$cid"; + + } elsif ( $sid eq "1029" ) { + return "usage: $cmd sid,qid" unless( defined($cid) ); + + #Warteschlange abspielen + $heosCmd = 'playQueueItem'; + $action = "qid=$cid"; + + } else { + if ( $sid > 0 && $sid < 1024 ) { + return "usage: $cmd sid,cid,mid" unless( defined($cid) && defined($mid) ); + + #Radio abspielen + $heosCmd = 'playStream'; + $action = "sid=$sid&cid=$cid&mid=$mid"; + + } else { + return "usage: $cmd sid,cid[,mid]" unless( defined($cid) ); + + #Server abspielen + $heosCmd = 'playPlaylist'; + $action = "sid=$sid&cid=$cid&aid=4"; + $action = "sid=$sid&cid=$cid&mid=$mid&aid=4" if ( defined($mid) ); + + } + } + + } else { + + #### alte get Befehle sollen raus + #### getPlayerInfo:noArg getPlayState:noArg getNowPlayingMedia:noArg getPlayMode:noArg + my $list = "play:noArg stop:noArg pause:noArg mute:on,off volume:slider,0,5,100 volumeUp:slider,0,1,10 volumeDown:slider,0,1,10 repeat:one,all,off shuffle:on,off next:noArg prev:noArg input"; + + my @players = devspec2array("TYPE=HEOSPlayer:FILTER=NAME!=$name"); + $list .= " groupWithMember:multiple-strict," . join( ",", @players ) if ( scalar @players > 0 ); + $list .= " groupClear:noArg" if ( defined($defs{"HEOSGroup".abs($pid)}) && $defs{"HEOSGroup".abs($pid)}->{STATE} eq "on" ); + + #Parameterlisten für FHEMWeb zusammen bauen + my $favoritcount = scalar(@{$hash->{IODev}{helper}{favorites}}) if ( defined $hash->{IODev}{helper}{favorites} ); + if ( defined $favoritcount && $favoritcount > 0) { + + $list .= " channel:slider,1,1,".scalar(@{$hash->{IODev}{helper}{favorites}}); + $list .= " channelUp:noArg channelDown:noArg" if ( $favoritcount > 1) + } + + if ( defined($hash->{helper}{queue}) && ref($hash->{helper}{queue}) eq "ARRAY" && scalar(@{$hash->{helper}{queue}}) > 0 ) { + + $list .= " playQueueItem:slider,1,1,".scalar(@{$hash->{helper}{queue}}) if ( defined $hash->{helper}{queue} ); + $list .= " clearQueue:noArg saveQueue"; + } + + if ( defined $hash->{IODev}{helper}{playlists} ) { + + my @playlists = map { my %n; $n{name} = $_->{name}; $n{name} =~ s/\s+/\ /g; $n{name} } (@{ $hash->{IODev}{helper}{playlists}}); + + $list .= " playPlaylist:".join(",",@playlists) if( scalar @playlists > 0 ); + $list .= " deletePlaylist:".join(",",@playlists) if( scalar @playlists > 0 ); + } + #print "List#########################################################\n".Dumper($list); + $list .= " aux:noArg" if ( exists $hash->{helper}{aux} ); + return "Unknown argument $cmd, choose one of $list"; + } + + $string .= "&$action" if( defined($action)); + IOWrite($hash,"$heosCmd","$string",undef); + Log3 $name, 4, "HEOSPlayer ($name) - IOWrite: $heosCmd $string IODevHash=$hash->{IODev}"; + return undef; +} + +sub HEOSPlayer_Parse($$) { + + my ($io_hash,$json) = @_; + my $name = $io_hash->{NAME}; + my $pid; + my $decode_json; + my $code; + + + $decode_json = decode_json(encode_utf8($json)); + Log3 $name, 4, "HEOSPlayer - ParseFn wurde aufgerufen"; + if( defined($decode_json->{pid}) ) { + + $pid = $decode_json->{pid}; + $code = abs($pid); + $code = $io_hash->{NAME} ."-". $code if( defined($io_hash->{NAME}) ); + + if( my $hash = $modules{HEOSPlayer}{defptr}{$code} ) { + + IOWrite($hash,'getPlayerInfo',"pid=$hash->{PID}",undef); + readingsSingleUpdate( $hash, "state", "on", 1 ); + Log3 $hash->{NAME}, 4, "HEOSPlayer ($hash->{NAME}) - find logical device: $hash->{NAME}"; + Log3 $hash->{NAME}, 4, "HEOSPlayer ($hash->{NAME}) - find PID in root from decode_json"; + return $hash->{NAME}; + + } else { + + my $devname = "HEOSPlayer".abs($pid); + return "UNDEFINED $devname HEOSPlayer $pid IODev=$name"; + } + + } else { + + my %message = map { my ( $key, $value ) = split "="; $key => $value } split('&', $decode_json->{heos}{message}); + + $pid = $message{pid} if( defined($message{pid}) ); + $pid = $decode_json->{payload}{pid} if( ref($decode_json->{payload}) ne "ARRAY" && defined($decode_json->{payload}{pid}) ); + + Log3 $name, 4, "HEOSPlayer ($name) PID: $pid"; + + $code = abs($pid); + $code = $io_hash->{NAME} ."-". $code if( defined($io_hash->{NAME}) ); + + if( my $hash = $modules{HEOSPlayer}{defptr}{$code} ) { + my $name = $hash->{NAME}; + + HEOSPlayer_WriteReadings($hash,$decode_json); + Log3 $name, 4, "HEOSPlayer ($name) - find logical device: $hash->{NAME}"; + + return $hash->{NAME}; + + } else { + + my $devname = "HEOSPlayer".abs($pid); + return "UNDEFINED $devname HEOSPlayer $pid IODev=$name"; + } + } +} + +sub HEOSPlayer_WriteReadings($$) { + + my ($hash,$decode_json) = @_; + my $name = $hash->{NAME}; + + + Log3 $name, 4, "HEOSPlayer ($name) - processing data to write readings"; + ############################ + #### Aufbereiten der Daten soweit nötig (bei Events zum Beispiel) + my $readingsHash = HEOSPlayer_PreProcessingReadings($hash,$decode_json) + if( $decode_json->{heos}{message} =~ /^pid=/ and $decode_json->{heos}{command} ne "player\/get_now_playing_media"); + + ############################ + #### schreiben der Readings + readingsBeginUpdate($hash); + ### Event Readings + if( ref($readingsHash) eq "HASH" ) { + + Log3 $name, 4, "HEOSPlayer ($name) - response json Hash back from HEOSPlayer_PreProcessingReadings"; + my $t; + my $v; + + while( ( $t, $v ) = each (%{$readingsHash}) ) { + readingsBulkUpdate( $hash, $t, $v ) if( defined( $v ) ); + } + } + + ### PlayerInfos + readingsBulkUpdate( $hash, 'name', $decode_json->{payload}{name} ); + readingsBulkUpdate( $hash, 'gid', $decode_json->{payload}{gid} ); + readingsBulkUpdate( $hash, 'model', $decode_json->{payload}{model} ); + readingsBulkUpdate( $hash, 'version', $decode_json->{payload}{version} ); + readingsBulkUpdate( $hash, 'network', $decode_json->{payload}{network} ); + readingsBulkUpdate( $hash, 'lineout', $decode_json->{payload}{lineout} ); + readingsBulkUpdate( $hash, 'control', $decode_json->{payload}{control} ); + readingsBulkUpdate( $hash, 'ip-address', $decode_json->{payload}{ip} ); + + ### playing Infos + readingsBulkUpdate( $hash, 'currentMedia', $decode_json->{payload}{type} ); + readingsBulkUpdate( $hash, 'currentTitle', $decode_json->{payload}{song} ); + readingsBulkUpdate( $hash, 'currentAlbum', $decode_json->{payload}{album} ); + readingsBulkUpdate( $hash, 'currentArtist', $decode_json->{payload}{artist} ); + readingsBulkUpdate( $hash, 'currentImageUrl', $decode_json->{payload}{image_url} ); + readingsBulkUpdate( $hash, 'currentMid', $decode_json->{payload}{mid} ); + readingsBulkUpdate( $hash, 'currentQid', $decode_json->{payload}{qid} ); + readingsBulkUpdate( $hash, 'currentSid', $decode_json->{payload}{sid} ); + readingsBulkUpdate( $hash, 'currentStation', $decode_json->{payload}{station} ); + + #sucht in den Favoriten nach der aktuell gespielten Radiostation und aktualisiert den channel wenn diese enthalten ist + my @presets = map { $_->{name} } (@{ $hash->{IODev}{helper}{favorites} }); + my $search = ReadingsVal($name,"currentStation" ,undef); + my( @index )= grep { $presets[$_] eq $search } 0..$#presets if ( defined $search ); + + readingsBulkUpdate( $hash, 'channel', $index[0]+1 ) if ( scalar @index > 0 ); + #readingsBulkUpdate( $hash, 'state', 'on' ); + readingsEndUpdate( $hash, 1 ); + Log3 $name, 5, "HEOSPlayer ($name) - readings set for $name"; + return undef; +} + + +############### +### my little Helpers + +sub HEOSPlayer_PreProcessingReadings($$) { + + my ($hash,$decode_json) = @_; + my $name = $hash->{NAME}; + my $reading; + my %buffer; + my %message = map { my ( $key, $value ) = split "="; $key => $value } split('&', $decode_json->{heos}{message}); + + + Log3 $name, 4, "HEOSPlayer ($name) - preprocessing readings"; + + if ( $decode_json->{heos}{command} =~ /play_state/ or $decode_json->{heos}{command} =~ /player_state_changed/ ) { + + $buffer{'playStatus'} = $message{state}; + + } elsif ( $decode_json->{heos}{command} =~ /volume_changed/ or $decode_json->{heos}{command} =~ /set_volume/ or $decode_json->{heos}{command} =~ /get_volume/ ) { + + my @value = split('&', $decode_json->{heos}{message}); + $buffer{'volume'} = $message{level}; + $buffer{'mute'} = $message{mute} if( $decode_json->{heos}{command} =~ /volume_changed/ ); + if (defined($buffer{'mute'}) && AttrVal($name, 'mute2play', 0) == 1) { + IOWrite($hash,'setPlayState',"pid=$hash->{PID}&state=play",undef) if $buffer{'mute'} eq "off"; + IOWrite($hash,'setPlayState',"pid=$hash->{PID}&state=stop",undef) if $buffer{'mute'} eq "on"; + } + + } elsif ( $decode_json->{heos}{command} =~ /play_mode/ or $decode_json->{heos}{command} =~ /repeat_mode_changed/ or $decode_json->{heos}{command} =~ /shuffle_mode_changed/ ) { + + $buffer{'shuffle'} = $message{shuffle}; + $buffer{'repeat'} = $message{repeat}; + $buffer{'repeat'} =~ s/.*\_(.*)/$1/g; + + } elsif ( $decode_json->{heos}{command} =~ /get_mute/ ) { + + $buffer{'mute'} = $message{state}; + + } elsif ( $decode_json->{heos}{command} =~ /volume_up/ or $decode_json->{heos}{command} =~ /volume_down/ ) { + + $buffer{'volumeUp'} = $message{step} if( $decode_json->{heos}{command} =~ /volume_up/ ); + $buffer{'volumeDown'} = $message{step} if( $decode_json->{heos}{command} =~ /volume_down/ ); + + } elsif ( $decode_json->{heos}{command} =~ /player_now_playing_changed/ or $decode_json->{heos}{command} =~ /favorites_changed/ ) { + IOWrite($hash,'getNowPlayingMedia',"pid=$hash->{PID}",undef); + + } elsif ( $decode_json->{heos}{command} =~ /play_preset/ ) { + + $buffer{'channel'} = $message{preset} + + } elsif ( $decode_json->{heos}{command} =~ /play_input/ ) { + + $buffer{'input'} = $message{input}; + + } elsif ( $decode_json->{heos}{command} =~ /playback_error/ ) { + + $buffer{'error'} = $message{error}; + + } else { + + Log3 $name, 4, "HEOSPlayer ($name) - no match found"; + return undef; + } + + Log3 $name, 4, "HEOSPlayer ($name) - Match found for decode_json"; + return \%buffer; +} + +sub HEOSPlayer_GetPlayerInfo($) { + + my $hash = shift; + + + RemoveInternalTimer($hash,'HEOSPlayer_GetPlayerInfo'); + IOWrite($hash,'getPlayerInfo',"pid=$hash->{PID}",undef); +} + +sub HEOSPlayer_GetPlayState($) { + + my $hash = shift; + + + RemoveInternalTimer($hash,'HEOSPlayer_GetPlayState'); + IOWrite($hash,'getPlayState',"pid=$hash->{PID}",undef); +} + +sub HEOSPlayer_GetPlayMode($) { + + my $hash = shift; + + + RemoveInternalTimer($hash,'HEOSPlayer_GetPlayMode'); + IOWrite($hash,'getPlayMode',"pid=$hash->{PID}",undef); +} + +sub HEOSPlayer_GetNowPlayingMedia($) { + + my $hash = shift; + + + RemoveInternalTimer($hash,'HEOSPlayer_GetNowPlayingMedia'); + IOWrite($hash,'getNowPlayingMedia',"pid=$hash->{PID}",undef); +} + +sub HEOSPlayer_GetVolume($) { + + my $hash = shift; + + + RemoveInternalTimer($hash,'HEOSPlayer_GetVolume'); + IOWrite($hash,'getVolume',"pid=$hash->{PID}",undef); +} + +sub HEOSPlayer_GetMute($) { + + my $hash = shift; + + + RemoveInternalTimer($hash,'HEOSPlayer_GetMute'); + IOWrite($hash,'getMute',"pid=$hash->{PID}",undef); +} + +sub HEOSPlayer_GetQueue($) { + + my $hash = shift; + + + RemoveInternalTimer($hash,'HEOSPlayer_GetQueue'); + IOWrite($hash,'getQueue',"pid=$hash->{PID}",undef); +} + +sub HEOSPlayer_MakePlayLink($$$$$$$$) { + + my ($type, $name, $sid, $itemtype, $itemname, $itemurl, $xsize, $ysize) = @_; + + if( $type eq 'FHEMWEB' ) { + + my $xcmd = 'cmd'.uri_escape('=get '.$name.' ls '.$sid); + my $xtext = $sid; + + $xcmd = 'cmd'.uri_escape('=set '.$name.' input '.$sid) if ( $itemtype eq "heos_queue" ); + $ysize = '10.75em' if (!defined($ysize)); + $xcmd = "FW_cmd('$FW_ME$FW_subdir?XHR=1&$xcmd')"; + + return '
    '.$itemname."
    "; + + } else { + + return sprintf( "%-15s %s\n", $sid, $itemname ); + + } + +} + +sub HEOSPlayer_makeImage($$) { + my ($url, $xsize, $ysize) = @_; + + my $ret .= "\n"; + + return $ret; +} + + + + + +1; + + + + + +=pod +=item device +=item summary Modul to controls the Denon multiroom soundsystem +=item summary_DE Modul zum steuern des Denon Multiroom-Soundsystem + +=begin html + + +

    HEOSPlayer

    +
      + HEOSPlayer +

      + In combination with HEOSMaster and HEOSGroup this FHEM Module controls the Denon multiroom soundsystem using a telnet socket connection and the HEOS Command Line Interface (CLI). +

      + Once the master device is created, the players and groups of Your system are automatically recognized and created in FHEM. From now on the players and groups can be controlled and changes in the HEOS app or at the Receiver are synchronized with the state and media readings of the players and groups. + +

      + Readings +
        +
      • channel - nr of now playing favorite
      • +
      • currentAlbum - name of now playing album
      • +
      • currentArtist - name of now playing artist
      • +
      • currentImageUrl - URL of cover art, station logo, etc.
      • +
      • currentMedia - type of now playing media (song|station|genre|artist|album|container)
      • +
      • currentMid - media ID
      • +
      • currentQid - queue ID
      • +
      • currentSid - source ID
      • +
      • currentStation - name of now playing station
      • +
      • currentTitle - name of now playing title
      • +
      • error - last error
      • +
      • gid - ID of group, in which player is member
      • +
      • ip-address - ip address of the player
      • +
      • lineout - lineout level type (variable|Fixed)
      • +
      • model - model of HEOS speaker (e.g. HEOS 1)
      • +
      • mute - player mute state (on|off)
      • +
      • name - name of player (received from app)
      • +
      • network - network connection type (wired|wifi)
      • +
      • playStatus - state of player (play|pause|stop)
      • +
      • repeat - player repeat state (on_all|on_one|off)
      • +
      • shuffle - player shuffle state (on|off)
      • +
      • state - state of player connection (on|off)
      • +
      • version - software version of HEOS speaker
      • +
      • volume - player volume level (0-100)
      • +
      • volumeDown - player volume step level (1-10, default 5)
      • +
      • volumeUp - player volume step level (1-10, default 5)
      • +
      +

      + + set +
        +
      • aux - uses source at aux-input of player
      • +
      • channel <nr> - plays favorite <nr> created with app
      • +
      • channelUp - switches to next favorite
      • +
      • channelDown- switches to previous favorite
      • +
      • clear queue - clears the queue
      • +
      • deletePlaylist <myList> - clears playlist <myList>
      • +
      • set <hp1> groupWithMember <hp2> - creates group with hp1 as leader and hp2 as member
      • +
      • input sid[,cid][,mid] - set input source-id[,container-id][,media-id]
      • +
          + Example: set kitchen input 1027,1772574848,inputs/tvaudio
          + starts "tv audio" on player "kitchen"
          +
        +
      • mute on|off - set mute state on|off
      • +
      • next - play next title in queue
      • +
      • pause - set state of player to "pause"
      • +
      • play - set state of player to "play"
      • +
      • playPlaylist <myList> - play playlist <myList>
      • +
      • playQueueItem <nr> - play title <nr> in queue
      • +
      • prev - play previous title in queue
      • +
      • repeat - set player repeat state (on_all|on_one|off)
      • +
      • saveQueue <myList> - save queue as <myList>
      • +
      • shuffle - set player shuffle state on|off
      • +
      • stop - set state of player to "stop"
      • +
      • volume - set volume 0..100
      • +
      • volumeDown - reduce volume by <volumeDown>
      • +
      • volumeUp - increase volume by <volumeUp>
      • +
      +

      + + get +
        +
      • ls - list music sources (input, playlists, favorites, music services, ...)
      • +
      • channelscount - number of favorites
      • +
      +

      + + state +
        +
      • state of player connection (on|off)
      • +
      +

      + + attributes +
        +
      • channelring - when reaching the last favorite ChannelUp/Down switches in circle, i.e. to the first/last favorite again
      • +
      • mute2play - if mute switch on speaker is pressed, the stream stops
      • +
      +
    + +=end html + +=begin html_DE + + +

    HEOSPlayer

    +
      + HEOSPlayer +

      + In Kombination mit HEOSMaster and HEOSGroup steuert dieses FHEM Modul das Denon Multiroom-Soundsystem mit Hilfe einer telnet Socket-Verbindung und dem HEOS Command Line Interface (CLI). +

      + Nachdem der Master einmal angelegt ist werden die Player und Gruppierungen des Systems automatisch erkannt und in FHEM angelegt. Von da an können die Player und Gruppierungen gesteuert werden und Veränderungen in der HEOS App oder am Reveiver werden mit dem Status und den Media Readings der Player und Gruppierungen synchronisiert. + +

      + Readings +
        +
      • channel - Nr des gerade abgespielten Favoriten
      • +
      • currentAlbum - Name des gerade abgespielten Albums
      • +
      • currentArtist - Name des gerade abgespielten Künstlers
      • +
      • currentImageUrl - URL des Albumcovers, Senderlogos, etc.
      • +
      • currentMedia - Medientyp des gerade abgespielten Streams (song|station|genre|artist|album|container)
      • +
      • currentMid - media ID
      • +
      • currentQid - queue ID
      • +
      • currentSid - source ID
      • +
      • currentStation - Name des gerade abgespielten Senders
      • +
      • currentTitle - Name des gerade abgespielten Titels
      • +
      • error - letzte Fehlermeldung
      • +
      • gid - ID der Gruppe, in der der Player Mitglied ist
      • +
      • ip-address - IP-Adresse des Players
      • +
      • lineout - lineout level type (variable|Fixed)
      • +
      • model - Modell des HEOS Lautsprechers (z.B. HEOS 1)
      • +
      • mute - Player mute Status (on|off)
      • +
      • name - Name des Players (aus der App übernommen)
      • +
      • network - Netzwerkverbindung (wired|wifi)
      • +
      • playStatus - Status des Players (play|pause|stop)
      • +
      • repeat - Player Repeat Status (on_all|on_one|off)
      • +
      • shuffle - Player Shuffle Status (on|off)
      • +
      • state - Status der Player-Verbindung (on|off)
      • +
      • version - Softwareversion des HEOS Lautsprechers
      • +
      • volume - aktuelle Lautstärke (0-100)
      • +
      • volumeDown - Schrittweite Lautstärke (1-10, default 5)
      • +
      • volumeUp - Schrittweite Lautstärke (1-10, default 5)
      • +
      +

      + + set +
        +
      • aux - aktiviert die Quelle am AUX-Eingang des Players
      • +
      • channel <nr> - spielt den vorher mit der App erstellten Favoriten <nr> ab
      • +
      • channelUp - schaltet auf den nächsten Favoriten in der Favoritenliste um
      • +
      • channelDown- schaltet auf vorherigen Favoriten in der Favoritenliste um
      • +
      • clear queue - löscht die Warteschlange
      • +
      • deletePlaylist <myList> - löscht die Playlist <myList>
      • +
      • set <hp1> groupWithMember <hp2> - erzeugt eine Gruppierung mit hp1 als Leader und hp2 als Mitglied
      • +
      • input sid[,cid][,mid] - setze input source-id[,container-id][,media-id]
      • +
          + Beispiel: set Küche input 1027,1772574848,inputs/tvaudio
          + startet "TV-Audio" auf dem Player "Küche"
          +
        +
      • mute on|off - setzt den mute Status on|off
      • +
      • next - spielt nächsten Titel in Warteschlange
      • +
      • pause - setzt den Status des Players auf "pause"
      • +
      • play - setzt den Status des Players auf "play"
      • +
      • playPlaylist <myList> - spielt die Playlist <myList> ab
      • +
      • playQueueItem <nr> - spielt Titel <nr> in Warteschlange
      • +
      • prev - spielt vorherigen Titel in Warteschlange
      • +
      • repeat - setzt den Player Repeat Status (on_all|on_one|off)
      • +
      • saveQueue <myList> - speichert die Warteschlange als Playlist <myList>
      • +
      • shuffle - setzt den Player Shuffle Status auf on|off
      • +
      • stop - setzt den Status des Players auf "stop"
      • +
      • volume - setzt die Lautstärke auf 0..100
      • +
      • volumeDown - verringert die Lautstärke um <volumeDown>
      • +
      • volumeUp - erhöht die Lautstärke um <volumeUp>
      • +
      +

      + + get +
        +
      • ls - listet Musikquellen (Eingänge, Playlists, Favoriten, Musik-Dienste, ...)
      • +
      • channelscount - Anzahl der Favoriten
      • +
      +

      + + state +
        +
      • Status der Player-Verbindung (on|off)
      • +
      +

      + + attributes +
        +
      • channelring - Beim Erreichen des letzten Favoriten schaltet ChannelUp/Down im Kreis, also wieder auf den ersten/letzten Favoriten
      • +
      • mute2play - Beim Betätigen der Mute-Taste am Lautsprecher wird auch der Stream angehalten
      • +
      +
    + +=end html_DE + +=cut diff --git a/fhem/MAINTAINER.txt b/fhem/MAINTAINER.txt index 51d289b4e..deb410453 100644 --- a/fhem/MAINTAINER.txt +++ b/fhem/MAINTAINER.txt @@ -122,6 +122,9 @@ FHEM/21_OWTEMP.pm mfr69bs http://forum.fhem.de 1Wire (de FHEM/21_OWTHERM.pm pahenning http://forum.fhem.de 1Wire FHEM/21_OWVAR.pm pahenning http://forum.fhem.de 1Wire FHEM/21_SONOSPLAYER Reinerlein http://forum.fhem.de Multimedia +FHEM/21_HEOSMaster CoolTux http://forum.fhem.de Multimedia +FHEM/21_HEOSPlayer CoolTux http://forum.fhem.de Multimedia +FHEM/21_HEOSGroup CoolTux http://forum.fhem.de Multimedia FHEM/22_ALL3076.pm sachag http://forum.fhem.de Sonstiges FHEM/22_HOMEMODE.pm DeeSPe http://forum.fhem.de Automatisierung FHEM/23_ALL4027.pm sachag http://forum.fhem.de Sonstiges