From bd172521718d57fe85e4a521a51451c3c72385ad Mon Sep 17 00:00:00 2001 From: Julian Pawlowski Date: Tue, 4 Jun 2019 17:11:49 +0200 Subject: [PATCH] initial commit --- 75_MSG.pm | 2519 +++++++++++++++++++++++++++++++++++++++++++++++++++++ README.md | 17 + 2 files changed, 2536 insertions(+) create mode 100755 75_MSG.pm create mode 100644 README.md diff --git a/75_MSG.pm b/75_MSG.pm new file mode 100755 index 0000000..d8a63c6 --- /dev/null +++ b/75_MSG.pm @@ -0,0 +1,2519 @@ +############################################################################### +# $Id: 75_MSG.pm 19215 2019-04-18 11:55:19Z loredo $ +# +#TODO +# - be able to use type "default" to let read from attr +# - forceType/forceDevice in user parameters +# - implement default messages in RESIDENTS using msg command +# - queue message until recipient is available again (e.g. when absent) +# also see https://forum.fhem.de/index.php/topic,69683.0.html +# - new msgType "queue" +# - escalation to type "queue" when n/a +# - automatically trigger to release queue messages by arriving at home +# (ROOMMATE) +# - allow some other ? to only reach people when they are at home +# - if ROOMMATE is asleep, queue message for next day +# (usefull escalate for screen with PostMe?) +# - delivery options as attributes (like ! or ? to gateways, devices or types) +# - all messages should be queued and then delivered so a timer may come back +# and check the gateway device for successful delivery +# - fix readings: store texts with \x{0000} notation +# - end user documentation / commandref !! +# +package main; +use strict; +use warnings; +use Data::Dumper; +use Time::HiRes qw(time); +use Encode; +use utf8; + +# initialize ################################################################## +sub MSG_Initialize($$) { + my %hash = ( + Fn => "CommandMsg", + Hlp => +"[] [<\@device>|] [] [||] <message-text>", + ); + $cmds{msg} = \%hash; + + require "$attr{global}{modpath}/FHEM/msgSchema.pm"; +} + +# regular Fn ################################################################## +sub CommandMsg($$;$$); +sub CommandMsg($$;$$) { + my ( $cl, $msg, $testMode ) = @_; + my $return = ""; + + if ( $featurelevel >= 5.7 ) { + my %dummy; + my ( $err, @a ) = ReplaceSetMagic( \%dummy, 0, ($msg) ); + $msg = join( " ", @a ) + unless ($err); + } + + # find existing msgConfig device or create a new instance + my $globalDevName = "globalMsg"; + if ( defined( $modules{msgConfig}{defptr} ) ) { + $globalDevName = $modules{msgConfig}{defptr}{NAME}; + } + else { + fhem "define $globalDevName msgConfig"; + $return .= + "Global configuration device $globalDevName was created.\n\n"; + } + + if ( $msg eq "" || $msg =~ /^\?[\s\t ]*$/ || $msg eq "help" ) { + return $return + . "Usage: msg [<type>] [<\@device>|<e-mail address>] [<priority>] [|<title>|] <message>"; + } + + # default settings + my $cmdSchema = msgSchema::get(); + my $settings = { + audio => { + typeEscalation => { + gwUnavailable => 'text', + emergency => 'text', + residentGone => 'text', + residentAbsent => 'text', + }, + }, + + light => { + typeEscalation => { + gwUnavailable => 'audio', + emergency => 'audio', + residentGone => 'audio', + residentAbsent => 'audio', + }, + }, + + push => { + typeEscalation => { + gwUnavailable => 'mail', + emergency => 'mail', + }, + }, + + screen => { + typeEscalation => { + gwUnavailable => 'light', + emergency => 'light', + residentGone => 'light', + residentAbsent => 'light', + }, + }, + + }; + + ################################################################ + ### extract message details + ### + + my ( $msgA, $params ) = parseParams($msg, "[^\\S\\n]", " "); + + # only use output from parseParams when + # parameters where found + if ( ref($params) eq "HASH" && keys %$params ) { + if ( scalar @$msgA > 0 ) { + $msg = join( " ", @$msgA ); + } + else { + $msg = ""; + } + } + + if ( defined( $params->{msgText} ) ) { + Log3 $globalDevName, 5, + "msg: Adding message text from given user parameters"; + $msg .= " " unless ( $msg eq "" ); + $msg .= $params->{msgText}; + delete $params->{msgText}; + } + + return $return + . "Usage: msg [<type>] [<\@device>|<e-mail address>] [<priority>] [|<title>|] <message>" + if ( $msg =~ m/^[\s\t\n ]*$/ ); + + Log3 $globalDevName, 5, "msg: Extracted user parameters\n" . Dumper($params) + if ( ref($params) eq "HASH" && keys %$params ); + + my $types = ""; + my $recipients = ""; + my $priority = ""; + my $title = "-"; + my $priorityCat = ""; + + # check for message types + if ( $params->{msgType} ) { + Log3 $globalDevName, 5, "msg: given types=$params->{msgType}"; + $types = $params->{msgType}; + $types =~ s/[\s\t ]+//g; + delete $params->{msgType}; + } + elsif ( $msg =~ +s/^[\s\t ]*([a-z,]*!?(screen|light|audio|text|push|mail|queue)[\w,!?&|]*)[\s\t ]+//i + ) + { + Log3 $globalDevName, 5, "msg: found types=$1" + unless ( defined($testMode) && $testMode eq "1" ); + $types = $1; + } + + # programatic exception: + # e.g. recipients were given automatically from empty readings + if ( + $msg =~ m/^[\s\t ]*([!]?(([A-Za-z0-9%+._-])*@([,\-:|]+)))[\s\t ]+/ + || ( $params->{msgRcpt} + && $params->{msgRcpt} =~ + m/^[\s\t ]*([!]?(([A-Za-z0-9%+._-])*@([,\-:|]+)))[\s\t ]+/ ) + ) + { + Log3 $globalDevName, 4, + "msg: message won't be sent - recipient '$1' contains special" + . " characters like ',-:|' or behind the @ character is simply" + . " emptiness. This might be okay, e.g. if you are using something" + . " like a reading from RESIDENTS/ROOMMATE/GUEST to address present" + . " or absent residents and this list is simply empty at this time." + . " ($msg)"; + return; + } + + # check for given recipients + if ( $params->{msgRcpt} ) { + Log3 $globalDevName, 5, "msg: given recipient=$params->{msgRcpt}"; + $recipients = $params->{msgRcpt}; + $recipients =~ s/[\s\t ]+//g; + delete $params->{msgRcpt}; + } + elsif ( $msg =~ +s/^[\s\t ]*([!]?(([A-Za-z0-9%+._-])*@([%+a-z0-9A-Z.-]+))[\w,@.!?|:]*)[\s\t ]+// + ) + { + Log3 $globalDevName, 5, "msg: found recipient=$1" + unless ( defined($testMode) && $testMode eq "1" ); + $recipients = $1; + } + + # check for given priority + if ( defined( $params->{msgPrio} ) ) { + Log3 $globalDevName, 5, "msg: given priority=$params->{msgPrio}"; + $priority = $params->{msgPrio}; + $priority =~ s/[\s\t ]+//g; + delete $params->{msgPrio}; + } + elsif ( $msg =~ s/^[\s\t ]*([-+]{0,1}\d+[\.\d]*)[\s\t ]*// ) { + Log3 $globalDevName, 5, "msg: found priority=$1" + unless ( defined($testMode) && $testMode eq "1" ); + + $priority = $1; + } + $priority = int($priority) if ( $priority =~ /^[-+]{0,1}\d+\.\d*$/ ); + return "Invalid priority $priority: Needs to be an integer value" + unless ( $priority eq "" || $priority =~ /^[-+]{0,1}\d+$/ ); + + # check for given message title + if ( defined( $params->{msgTitle} ) ) { + Log3 $globalDevName, 5, "msg: given title=$params->{msgTitle}"; + $title = $params->{msgTitle}; + $title =~ s/^[\s\t ]*\|(.*?)\|[\s\t ]*/$1/; + delete $params->{msgTitle}; + } + elsif ( $msg =~ s/^[\s\t ]*\|(.*?)\|[\s\t ]*// ) { + Log3 $globalDevName, 5, "msg: found title=$1" + unless ( defined($testMode) && $testMode eq "1" ); + + $title = $1; + } + + # check for user parameters (DEPRECATED / legacy compatibility only) + if ( $msg =~ s/[\s\t ]*O(\[\{.*\}\])[\s\t ]*$// ) { + + Log3 $globalDevName, 5, "msg: found options=$1" + unless ( defined($testMode) && $testMode eq "1" ); + + # Use JSON module if possible + eval { + require JSON::PP; + import JSON::PP qw( decode_json ); + }; + if ($@) { + Log3 $globalDevName, 3, + "msg: Error loading JSON::PP. " + . "Please switch to new syntax to use user parameters"; + } + else { + Log3 $globalDevName, 4, + "msg: Please switch to new syntax to use user parameters"; + my $o; + eval '$o = decode_json( encode_utf8($1) ); 1'; + if ($@) { + Log3 $globalDevName, 5, + "msg: Error decoding JSON for user parameters: $@"; + } + elsif ( ref($o) eq "ARRAY" ) { + + for my $item (@$o) { + next unless ( ref($item) eq "HASH" ); + for my $key ( keys(%$item) ) { + next if ( ref( $item->{$key} ) ); + my $val = $item->{$key}; + $params->{$key} = $item->{$key} + unless ( $params->{$key} ); + } + } + + Log3 $globalDevName, 5, + "msg: Decoded user parameters\n" . Dumper($params) + if ($params); + } + } + } + + $types = "default" + if ( $types eq "" ); + my $msgSent = 0; + my $forwarded = ""; + my %sentTypesPerDevice; + my $sentCounter = 0; + my $msgID = time(); + my $msgDateTime = TimeNow(); + my $isTypeOr = 1; + my $isRecipientOr = 1; + my $hasTypeOr = 0; + my $hasRecipientOr = 0; + my $softFail = 0; + $recipients = "\@" . $globalDevName if ( $recipients eq "" ); + + ################################################################ + ### recipient loop + ### + + my @recipientsOr = split( /\|/, $recipients ); + Log3 $globalDevName, 6, + "msg: recipientOr total is " . scalar( grep { defined $_ } @recipientsOr ) + unless ( defined($testMode) && $testMode eq "1" ); + + for ( + my $iRecipOr = 0 ; + $iRecipOr < scalar( grep { defined $_ } @recipientsOr ) ; + $iRecipOr++ + ) + { + Log3 $globalDevName, 6, + "msg: " + . "running loop recipientsOr for recipient(s) $recipientsOr[$iRecipOr]" + unless ( defined($testMode) && $testMode eq "1" ); + my $loopReturn1 = ""; + + $hasRecipientOr = 1 + if ( scalar( grep { defined $_ } @recipientsOr ) > 1 ); + + my @recipient = split( /,/, $recipientsOr[$iRecipOr] ); + foreach my $device (@recipient) { + Log3 $globalDevName, 6, "msg: running loop for device $device" + unless ( defined($testMode) && $testMode eq "1" ); + my $loopReturn2 = ""; + + ################################################################ + ### type loop + ### + + my @typesOr = split( /\|/, $types ); + Log3 $globalDevName, 6, + "msg: typeOr total is " . scalar( grep { defined $_ } @typesOr ) + unless ( defined($testMode) && $testMode eq "1" ); + + for ( + my $iTypesOr = 0 ; + $iTypesOr < scalar( grep { defined $_ } @typesOr ) ; + $iTypesOr++ + ) + { + Log3 $globalDevName, 6, + "msg: running loop typeOr for type(s) $typesOr[$iTypesOr]" + unless ( defined($testMode) && $testMode eq "1" ); + my $loopReturn3 = ""; + + $hasTypeOr = 1 + if ( scalar( grep { defined $_ } @typesOr ) > 1 ); + + my @type = split( /,/, lc( $typesOr[$iTypesOr] ) ); + for ( + my $i = 0 ; + $i < scalar( grep { defined $_ } @type ) ; + $i++ + ) + { + Log3 $globalDevName, 6, + "msg: running loop for type $type[$i]" + unless ( defined($testMode) && $testMode eq "1" ); + last unless ( defined( $type[$i] ) ); + + # check for correct type + my @msgCmds = ( + "screen", "light", "audio", "text", + "push", "mail", "queue", "default" + ); + unless ( grep { $type[$i] =~ /^$_/i } @msgCmds ) { + $loopReturn3 .= "Unknown message type $type[$i]\n"; + next; + } + + ### + ### /type loop + ################################################################ + + my @unavailabilityIndicators = ( + "0", "false", + "absent", "disappeared", + "unauthorized", "unavailable", + "unreachable", "disconnected" + ); + + my $logDevice; + $logDevice = $globalDevName; + $logDevice = $device + if ( + MSG_FindAttrVal( $device, "verbose", undef, undef ) ); + + my $msgSentDev = 0; + my $gatewayDevs = ""; + my $forceDevice = 0; + my $forceQueue = 0; + + # for device type + my $deviceType = "device"; + if ( $device =~ + m/^(([A-Za-z0-9%+._-])+@+([%+a-z0-9A-Z.-]*))$/ ) + { + $gatewayDevs = $1; + $deviceType = "email"; + } + elsif ( $device =~ + m/^@?([A-Za-z\d_\.\-\/]+)([^A-Za-z\d_\.\-\/]+)[\s\t ]*$/ + ) + { + $device = $1; + + foreach ( split( /(\S)/, $2 ) ) { + $forceDevice = 1 if ( $_ eq "!" ); + $softFail = 1 if ( $_ eq "?" ); + $forceQueue = 1 if ( $_ eq "&" ); + } + + Log3 $logDevice, 5, +"msg $device: forceDevice=$forceDevice (from device contact)" + if ($forceDevice); + Log3 $logDevice, 5, +"msg $device: softFail=$softFail (from device contact)" + if ($softFail); + Log3 $logDevice, 5, +"msg $device: forceQueue=$forceQueue (from device contact)" + if ($forceQueue); + } + elsif ( $device =~ /^@(.*)/ ) { + $device = $1; + } + + # sub-recipient + my $subRecipient = ""; + my $termRecipient = ""; + if ( $device =~ +m/^@?([A-Za-z0-9._]+):([A-Za-z0-9._\-\/@+]*):?([A-Za-z0-9._\-\/@+]*)$/ + ) + { + $device = $1; + $subRecipient = $2; + $termRecipient = $3; + } + + # FATAL ERROR: device does not exist + if ( !IsDevice($device) + && $deviceType eq "device" ) + { + $loopReturn3 .= "Device $device does not exist\n" + unless ($softFail); + unless ( defined($testMode) && $testMode eq "1" ) { + Log3 $logDevice, 2, + "msg $device: Device does not exist" + unless ($softFail); + Log3 $logDevice, 5, + "msg $device: Device does not exist" + if ($softFail); + } + + my $regex1 = + "\\s*!?@?" . $device . "[,|]"; # at the beginning + my $regex2 = "[,|]!?@?" . $device . "\\s*"; # at the end + my $regex3 = + ",!?@?" . $device . ","; # in the middle with comma + my $regex4 = + "[\|,]!?@?" + . $device + . "[\|,]"; # in the middle with pipe and/or comma + + $recipients =~ s/^$regex1//gi; + $recipients =~ s/$regex2$/|/gi; + $recipients =~ s/$regex3/,/gi; + $recipients =~ s/$regex4/|/gi; + + next; + } + + # Find custom types for devices + if ( $type[$i] eq "default" ) { + delete $typesOr[$iTypesOr] + if ( $typesOr[$iTypesOr] eq "default" ); + delete $type[$i]; + Log3 $logDevice, 5, + "msg $device: msgType lookup for $device:"; + + my @t = split( + m/\|/, + MSG_FindAttrVal( + $device, "msgType", undef, "text" + ) + ); + $hasTypeOr = 1 + if ( scalar( grep { defined $_ } @t ) > 1 ); + + foreach (@t) { + Log3 $logDevice, 5, + "msg $device: Adding to \@typesOr: $_"; + + push @typesOr, $_; + + foreach ( split( /,/, lc($_) ) ) { + Log3 $logDevice, 5, + "msg $device: Adding to \@type: $_"; + push @type, $_; + } + } + } + + my $forceType = 0; + if ( $type[$i] =~ + m/^([A-Za-z\d_\.\-\/]+)([^A-Za-z\d_\.\-\/]+)[\s\t ]*$/ ) + { + $type[$i] = $1; + + foreach ( split( /(\S)/, $2 ) ) { + $forceType = 1 if ( $_ eq "!" ); + $softFail = 1 if ( $_ eq "?" ); + $forceQueue = 1 if ( $_ eq "&" ); + } + + Log3 $logDevice, 5, + "msg $device: forceType=$forceType (from type)" + if ($forceType); + Log3 $logDevice, 5, + "msg $device: softFail=$softFail (from type)" + if ($softFail); + Log3 $logDevice, 5, + "msg $device: forceQueue=$forceQueue (from type)" + if ($forceQueue); + } + + # next type loop if device is an email address + # and this is not the mail type loop run + if ( $deviceType eq "email" + && $type[$i] ne "mail" + && $type[$i] ne "text" ) + { + Log3 $logDevice, 5, + "msg $device: " + . "Skipping loop for device type 'email' with unmatched message type '" + . $type[$i] . "'"; + next; + } + + my $typeUc = ucfirst( $type[$i] ); + my $catchall = 0; + my $useLocation = 0; + + ################################################################ + ### get target information from device location + ### + + # search for location references + my @locationDevs; + @locationDevs = split( + /,/, + MSG_FindAttrVal( + $device, "msgLocationDevs", $typeUc, "" + ) + ); + + if ( $deviceType eq "device" ) { + + # get device location + my $deviceLocation = + msgConfig_FindReadingsVal( $device, "location", + $typeUc, "" ); + + my $locationDev = ""; + if ( $deviceLocation ne "" && $deviceType eq "device" ) + { + + # lookup matching location + foreach (@locationDevs) { + + if ( $featurelevel >= 5.7 ) { + my %dummy; + my ( $err, @a ) = + ReplaceSetMagic( \%dummy, 0, ($_) ); + $_ = join( " ", @a ) + unless ($err); + } + + my $lName = + AttrVal( $_, "msgLocationName", "" ); + if ( $lName ne "" && $lName eq $deviceLocation ) + { + $locationDev = $_; + last; + } + } + + if ( $featurelevel >= 5.7 ) { + my %dummy; + my ( $err, @a ) = + ReplaceSetMagic( \%dummy, 0, ($locationDev) ); + $locationDev = join( " ", @a ) + unless ($err); + } + + # look for gateway device + $gatewayDevs = + MSG_FindAttrVal( $locationDev, "msgContact", + $typeUc, "" ); + + # at least one of the location gateways needs to + # be available. Otherwise we fall back to + # non-location contacts + if ( $gatewayDevs ne "" ) { + + if ( $featurelevel >= 5.7 ) { + my %dummy; + my ( $err, @a ) = + ReplaceSetMagic( \%dummy, 0, + ($gatewayDevs) ); + $gatewayDevs = join( " ", @a ) + unless ($err); + } + + foreach + my $gatewayDevOr ( split /\|/, $gatewayDevs ) + { + + foreach my $gatewayDev ( split /,/, + $gatewayDevOr ) + { + my $tmpSubRecipient; + if ( $gatewayDev =~ /:(.*)/ ) { + $tmpSubRecipient = $1; + } + + if ( $type[$i] ne "mail" + && !IsDevice($gatewayDev) + && $deviceType eq "device" ) + { + $useLocation = 2 + if ( $useLocation == 0 ); + } + elsif ( $type[$i] ne "mail" + && IsDisabled($gatewayDev) ) + { + $useLocation = 2 + if ( $useLocation == 0 ); + } + elsif ( + $type[$i] ne "mail" + && ( + ( + grep { + ReadingsVal( + $gatewayDev, + "presence", + "present" + ) eq $_ + } @unavailabilityIndicators + ) + + || ( + grep { + ReadingsVal( + $gatewayDev, + "state", + "present" + ) eq $_ + } @unavailabilityIndicators + ) + + || ( + defined( + $defs{$gatewayDev} + ) + && defined( + $defs{$gatewayDev} + {STATE} + ) + && ( + grep { + $defs{$gatewayDev} + {STATE} eq $_ + } @unavailabilityIndicators + ) + ) + + || ReadingsVal( + $gatewayDev, "available", + "yes" + ) =~ m/^(0|no|false)$/i + + || ReadingsVal( + $gatewayDev, "reachable", + "yes" + ) =~ m/^(0|no|false)$/i + ) + ) + { + $useLocation = 2 + if ( $useLocation == 0 ); + } + else { + $useLocation = 1; + } + + } + + } + + # use gatewayDevs from location only + # if it has been confirmed to be available + if ( $useLocation == 1 ) { + Log3 $logDevice, 4, "msg $device: " + . "Matching location definition found"; + } + else { + $gatewayDevs = ""; + } + } + } + } + + ################################################################ + ### given device name is already a gateway device itself + ### + + my $deviceType2 = GetType($device); + + if ( + $gatewayDevs eq "" + && $deviceType eq "device" + && $deviceType2 ne "" + && ( + ( + $type[$i] eq "audio" && defined( + $cmdSchema->{ $type[$i] }{$deviceType2} + ) + ) + || ( + $type[$i] eq "light" + && defined( + $cmdSchema->{ $type[$i] }{$deviceType2} + ) + ) + || ( + $type[$i] eq "push" + && defined( + $cmdSchema->{ $type[$i] }{$deviceType2} + ) + ) + || ( + $type[$i] eq "screen" + && defined( + $cmdSchema->{ $type[$i] }{$deviceType2} + ) + ) + || ( + $type[$i] eq "queue" + && defined( + $cmdSchema->{ $type[$i] }{$deviceType2} + ) + ) + ) + ) + { + Log3 $logDevice, 4, + "msg $device: Recipient type $deviceType2 " + . "is a gateway device itself for message type " + . $type[$i] + . ". Still checking for any delegates ..." + unless ( defined($testMode) && $testMode eq "1" ); + + $gatewayDevs = + MSG_FindAttrVal( $device, "msgContact", $typeUc, + $device ); + } + + ################################################################ + ### get target information from device + ### + + elsif ( $deviceType eq "device" && $gatewayDevs eq "" ) { + + # look for gateway device + $gatewayDevs = + MSG_FindAttrVal( $device, "msgContact", $typeUc, "" ); + + # fallback/catchall + if ( $gatewayDevs eq "" ) { + $catchall = 1 + if ( $device ne $globalDevName ); + + Log3 $logDevice, 6, + "msg $device: (No $typeUc contact defined, " + . "trying global instead)" + if ( $catchall == 1 ); + + $gatewayDevs = + MSG_FindAttrVal( $globalDevName, "msgContact", + $typeUc, "" ); + } + } + + # Find priority if none was explicitly specified + my $loopPriority = $priority; + $loopPriority = + MSG_FindAttrVal( $device, "msgPriority$typeUc", $typeUc, + MSG_FindAttrVal( $device, "msgPriority", $typeUc, 0 ) ) + if ( $priority eq "" ); + + # check for available routes + # + my %routes; + $routes{screen} = 0; + $routes{light} = 0; + $routes{audio} = 0; + $routes{text} = 0; + $routes{push} = 0; + $routes{mail} = 0; + $routes{queue} = 1; + + if ( + !defined($testMode) + || ( $testMode ne "1" + && $testMode ne "2" ) + ) + { + Log3 $logDevice, 5, + "msg $device: Checking for available routes " + . "(triggered by type $type[$i])"; + + $routes{screen} = 1 + if ( + $deviceType eq "device" + && CommandMsg( "screen", + "screen \@$device $priority Routing Test", 1 ) + eq "ROUTE_AVAILABLE" + ); + + $routes{light} = 1 + if ( + $deviceType eq "device" + && CommandMsg( "light", + "light \@$device $priority Routing Test", 1 ) + eq "ROUTE_AVAILABLE" + ); + + $routes{audio} = 1 + if ( + $deviceType eq "device" + && CommandMsg( "audio", + "audio \@$device $priority Routing Test", 1 ) + eq "ROUTE_AVAILABLE" + ); + + if ( + $deviceType eq "device" + && CommandMsg( "push", + "push \@$device $priority Routing Test", 1 ) eq + "ROUTE_AVAILABLE" + ) + { + $routes{push} = 1; + $routes{text} = 1; + } + + if ( + CommandMsg( "mail", + "mail \@$device $priority Routing Test", 1 ) eq + "ROUTE_AVAILABLE" + ) + { + $routes{mail} = 1; + $routes{text} = 1; + } + + $routes{mail} = 1 + if ( $deviceType eq "email" ); + + Log3 $logDevice, 4, + "msg $device: Available routes: screen=" + . $routes{screen} + . " light=" + . $routes{light} + . " audio=" + . $routes{audio} + . " text=" + . $routes{text} + . " push=" + . $routes{push} + . " mail=" + . $routes{mail}; + } + + ################################################## + ### dynamic routing for text (->push, ->mail) + ### + if ( $type[$i] eq "text" ) { + + # user selected emergency priority text threshold + my $prioThresTextEmg = + MSG_FindAttrVal( $device, "msgThPrioTextEmergency", + $typeUc, 2 ); + + # user selected low priority text threshold + my $prioThresTextNormal = + MSG_FindAttrVal( $device, "msgThPrioTextNormal", + $typeUc, -2 ); + + # Decide push and/or e-mail destination based + # on priorities + if ( $loopPriority >= $prioThresTextEmg + && $routes{push} == 1 + && $routes{mail} == 1 ) + { + Log3 $logDevice, 4, + "msg $device: " + . "Text routing decision: push+mail(1)"; + $forwarded .= "," + if ( $forwarded ne "" ); + $forwarded .= "text>push+mail"; + push @type, "push" + unless grep { "push" eq $_ } @type; + push @type, "mail" + unless grep { "mail" eq $_ } @type; + } + elsif ($loopPriority >= $prioThresTextEmg + && $routes{push} == 1 + && $routes{mail} == 0 ) + { + Log3 $logDevice, 4, + "msg $device: Text routing decision: push(2)"; + $forwarded .= "," + if ( $forwarded ne "" ); + $forwarded .= "text>push"; + push @type, "push" + unless grep { "push" eq $_ } @type; + } + elsif ($loopPriority >= $prioThresTextEmg + && $routes{push} == 0 + && $routes{mail} == 1 ) + { + Log3 $logDevice, 4, + "msg $device: Text routing decision: mail(3)"; + $forwarded .= "," + if ( $forwarded ne "" ); + $forwarded .= "text>mail"; + push @type, "mail" + unless grep { "mail" eq $_ } @type; + } + elsif ($loopPriority >= $prioThresTextNormal + && $routes{push} == 1 ) + { + Log3 $logDevice, 4, + "msg $device: Text routing decision: push(4)"; + $forwarded .= "," + if ( $forwarded ne "" ); + $forwarded .= "text>push"; + push @type, "push" + unless grep { "push" eq $_ } @type; + } + elsif ($loopPriority >= $prioThresTextNormal + && $routes{mail} == 1 ) + { + Log3 $logDevice, 4, + "msg $device: Text routing decision: mail(5)"; + $forwarded .= "," + if ( $forwarded ne "" ); + $forwarded .= "text>mail"; + push @type, "mail" + unless grep { "mail" eq $_ } @type; + } + elsif ( $routes{mail} == 1 ) { + Log3 $logDevice, 4, + "msg $device: Text routing decision: mail(6)"; + $forwarded .= "," + if ( $forwarded ne "" ); + $forwarded .= "text>mail"; + push @type, "mail" + unless grep { "mail" eq $_ } @type; + } + elsif ( $routes{push} == 1 ) { + Log3 $logDevice, 4, + "msg $device: Text routing decision: push(7)"; + $forwarded .= "," + if ( $forwarded ne "" ); + $forwarded .= "text>push"; + push @type, "push" + unless grep { "push" eq $_ } @type; + } + + # FATAL ERROR: routing decision failed + else { + Log3 $logDevice, 4, + "msg $device: " + . "Text routing FAILED - priority=$loopPriority push=" + . $routes{push} + . " mail=" + . $routes{mail}; + + $loopReturn3 .= + "ERROR: Could not find any Push or Mail contact " + . "for device $device - set attributes: msgContactPush " + . "| msgContactMail | msgContactText | msgRecipientPush " + . "| msgRecipientMail | msgRecipientText | msgRecipient\n"; + } + + next; + } + + # FATAL ERROR: we could not find any targets for + # user specified device... + if ( $gatewayDevs eq "" + && $device ne $globalDevName ) + { + $loopReturn3 .= + "ERROR: Could not find any $typeUc contact " + . "for device $device - set attributes: msgContact$typeUc | msgRecipient$typeUc | msgRecipient\n" + unless ( $type[$i] eq "queue" ); + } + + # FATAL ERROR: we could not find any targets at all + elsif ( $gatewayDevs eq "" ) { + $loopReturn3 .= + "ERROR: Could not find any general $typeUc contact. " + . "Please specify a destination device or set attributes in general msg configuration device $globalDevName : msgContact$typeUc | msgRecipient$typeUc | msgRecipient\n" + unless ( $type[$i] eq "queue" ); + } + + # user selected audio-visual announcement state + my $annState = ReadingsVal( + MSG_FindAttrVal( + $device, "msgSwitcherDev", $typeUc, "" + ), + "state", "long" + ); + + # user selected emergency priority audio threshold + my $prioThresAudioEmg = + MSG_FindAttrVal( $device, "msgThPrioAudioEmergency", + $typeUc, 2 ); + + # user selected high priority audio threshold + my $prioThresAudioHigh = + MSG_FindAttrVal( $device, "msgThPrioAudioHigh", $typeUc, + 1 ); + + # user selected high priority threshold + my $prioThresHigh = + MSG_FindAttrVal( $device, "msgThPrioHigh", $typeUc, 2 ); + + # user selected normal priority threshold + my $prioThresNormal = + MSG_FindAttrVal( $device, "msgThPrioNormal", $typeUc, 0 ); + + if ( $type[$i] eq "audio" ) { + if ( $annState eq "long" + || $forceType == 1 + || $forceDevice == 1 + || $loopPriority >= $prioThresAudioEmg ) + { + $priorityCat = ""; + } + elsif ( $loopPriority >= $prioThresAudioHigh ) { + $priorityCat = "ShortPrio"; + } + else { + $priorityCat = "Short"; + } + } + else { + if ( $loopPriority >= $prioThresHigh ) { + $priorityCat = "High"; + } + elsif ( $loopPriority >= $prioThresNormal ) { + $priorityCat = ""; + } + else { + $priorityCat = "Low"; + } + } + + # get resident presence information + # + my $residentDevState = ""; + my $residentDevPresence = ""; + + # device + if ( ReadingsVal( $device, "presence", "-" ) ne "-" ) { + $residentDevState = ReadingsVal( $device, "state", "" ); + $residentDevPresence = + ReadingsVal( $device, "presence", "" ); + } + + # device indirect + if ( + ( + $residentDevState eq "" + || $residentDevPresence eq "" + ) + && ReadingsVal( + AttrVal( $device, "msgRecipient$typeUc", "" ), + "presence", "-" ) ne "-" + ) + { + $residentDevState = + ReadingsVal( + AttrVal( $device, "msgRecipient$typeUc", "" ), + "state", "" ) + if ( $residentDevState eq "" ); + $residentDevPresence = + ReadingsVal( + AttrVal( $device, "msgRecipient$typeUc", "" ), + "presence", "" ) + if ( $residentDevPresence eq "" ); + } + + # device indirect general + if ( + ( + $residentDevState eq "" + || $residentDevPresence eq "" + ) + && ReadingsVal( AttrVal( $device, "msgRecipient", "" ), + "presence", "-" ) ne "-" + ) + { + $residentDevState = + ReadingsVal( AttrVal( $device, "msgRecipient", "" ), + "state", "" ) + if ( $residentDevState eq "" ); + $residentDevPresence = + ReadingsVal( AttrVal( $device, "msgRecipient", "" ), + "presence", "" ) + if ( $residentDevPresence eq "" ); + } + + # device explicit + if ( + ( + $residentDevState eq "" + || $residentDevPresence eq "" + ) + && ReadingsVal( + AttrVal( $device, "msgResidentsDev", "" ), + "presence", "-" ) ne "-" + ) + { + $residentDevState = + ReadingsVal( + AttrVal( $device, "msgResidentsDev", "" ), + "state", "" ) + if ( $residentDevState eq "" ); + $residentDevPresence = + ReadingsVal( + AttrVal( $device, "msgResidentsDev", "" ), + "presence", "" ) + if ( $residentDevPresence eq "" ); + } + + # global indirect + if ( + ( + $residentDevState eq "" + || $residentDevPresence eq "" + ) + && ReadingsVal( + AttrVal( + $globalDevName, "msgRecipient$typeUc", "" + ), + "presence", + "-" + ) ne "-" + ) + { + $residentDevState = ReadingsVal( + AttrVal( + $globalDevName, "msgRecipient$typeUc", "" + ), + "state", "" + ) if ( $residentDevState eq "" ); + $residentDevPresence = ReadingsVal( + AttrVal( + $globalDevName, "msgRecipient$typeUc", "" + ), + "presence", + "" + ) if ( $residentDevPresence eq "" ); + } + + # global indirect general + if ( + ( + $residentDevState eq "" + || $residentDevPresence eq "" + ) + && ReadingsVal( + AttrVal( $globalDevName, "msgRecipient", "" ), + "presence", "-" ) ne "-" + ) + { + $residentDevState = + ReadingsVal( + AttrVal( $globalDevName, "msgRecipient", "" ), + "state", "" ) + if ( $residentDevState eq "" ); + $residentDevPresence = + ReadingsVal( + AttrVal( $globalDevName, "msgRecipient", "" ), + "presence", "" ) + if ( $residentDevPresence eq "" ); + } + + # global explicit + if ( + ( + $residentDevState eq "" + || $residentDevPresence eq "" + ) + && ReadingsVal( + AttrVal( $globalDevName, "msgResidentsDev", "" ), + "presence", "-" ) ne "-" + ) + { + $residentDevState = + ReadingsVal( + AttrVal( $globalDevName, "msgResidentsDev", "" ), + "state", "" ) + if ( $residentDevState eq "" ); + $residentDevPresence = + ReadingsVal( + AttrVal( $globalDevName, "msgResidentsDev", "" ), + "presence", "" ) + if ( $residentDevPresence eq "" ); + } + + ################################################################ + ### Send message + ### + + my $queued = 0; + + # user selected emergency priority text threshold + my $prioThresGwEmg = + MSG_FindAttrVal( $device, "msgThPrioGwEmergency", + $typeUc, 2 ); + + if ( $featurelevel >= 5.7 ) { + my %dummy; + my ( $err, @a ) = + ReplaceSetMagic( \%dummy, 0, ($gatewayDevs) ); + $gatewayDevs = join( " ", @a ) + unless ($err); + } + + my %gatewaysStatus; + $gatewayDevs = $globalDevName if ( $type[$i] eq "queue" ); + + foreach my $gatewayDevOr ( split /\|/, $gatewayDevs ) { + foreach my $gatewayDev ( split /,/, $gatewayDevOr ) { + + if ( $gatewayDev =~ +m/^@?([A-Za-z0-9._]+):([A-Za-z0-9._\-\/@+]*):?([A-Za-z0-9._\-\/@+]*)$/ + ) + { + $gatewayDev = $1; + $subRecipient = $2 if ( $subRecipient eq "" ); + $termRecipient = $3 if ( $termRecipient eq "" ); + } + + my $logMsg = + "msg $device: " + . "Trying to send message via gateway $gatewayDev"; + $logMsg .= " to recipient $subRecipient" + if ( $subRecipient ne "" ); + $logMsg .= ", terminal device $termRecipient" + if ( $termRecipient ne "" ); + Log3 $logDevice, 5, $logMsg + unless ( defined($testMode) && $testMode eq "1" ); + + ############## + # check for gateway availability and set route status + # + + my $routeStatus = "OK"; + if ( $type[$i] eq "queue" ) { + $routeStatus = "OK_QUEUE"; + } + + elsif ($type[$i] ne "mail" + && !IsDevice($gatewayDev) + && $deviceType eq "device" ) + { + $routeStatus = "UNDEFINED"; + } + + elsif ( $type[$i] ne "mail" + && IsDisabled($gatewayDev) ) + { + $routeStatus = "DISABLED"; + } + + elsif ( + $type[$i] ne "mail" + && ( + ( + grep { + ReadingsVal( $gatewayDev, + "presence", "present" ) eq $_ + } @unavailabilityIndicators + ) + + || ( + grep { + ReadingsVal( $gatewayDev, "state", + "present" ) eq $_ + } @unavailabilityIndicators + ) + + || ( + IsDevice($gatewayDev) + && defined( $defs{$gatewayDev}{STATE} ) + && ( + grep { + $defs{$gatewayDev}{STATE} eq $_ + } @unavailabilityIndicators + ) + ) + + || ReadingsVal( $gatewayDev, "available", + "yes" ) =~ m/^(0|no|false)$/i + + || ReadingsVal( $gatewayDev, "reachable", + "yes" ) =~ m/^(0|no|false)$/i + ) + ) + { + $routeStatus = "UNAVAILABLE"; + } + + elsif ( + $type[$i] ne "mail" + && ( + ReadingsVal( $gatewayDev, "presence", + "present" ) =~ +m/^(0|false|absent|disappeared|unauthorized|disconnected|unreachable)$/i + || ReadingsVal( $gatewayDev, "state", + "present" ) =~ +m/^(absent|disappeared|unauthorized|disconnected|unreachable)$/i + || ( IsDevice($gatewayDev) + && defined( $defs{$gatewayDev}{STATE} ) + && $defs{$gatewayDev}{STATE} =~ +m/^(absent|disappeared|unauthorized|disconnected|unreachable)$/i + ) + || ReadingsVal( $gatewayDev, "available", + "yes" ) =~ m/^(0|no|off|false)$/i + || ReadingsVal( $gatewayDev, "reachable", + "yes" ) =~ m/^(0|no|off|false)$/i + ) + ) + { + $routeStatus = "UNAVAILABLE"; + } + + elsif ( $type[$i] eq "screen" + && ReadingsVal( $gatewayDev, "power", "on" ) =~ + m/^(0|off)$/i ) + { + $routeStatus = "OFF"; + } + + elsif ($type[$i] eq "audio" + && $annState ne "long" + && $annState ne "short" ) + { + $routeStatus = "USER_DISABLED"; + } + + elsif ( $type[$i] eq "light" && $annState eq "off" ) + { + $routeStatus = "USER_DISABLED"; + } + + elsif ($type[$i] ne "push" + && $type[$i] ne "mail" + && $residentDevPresence eq "absent" ) + { + $routeStatus = "USER_ABSENT"; + } + + elsif ($type[$i] ne "push" + && $type[$i] ne "mail" + && $residentDevState eq "asleep" ) + { + $routeStatus = "USER_ASLEEP"; + } + + # enforce by user request + if ( + ( + $routeStatus eq "USER_DISABLED" + || $routeStatus eq "USER_ABSENT" + || $routeStatus eq "USER_ASLEEP" + ) + && ( $forceType == 1 || $forceDevice == 1 ) + ) + { + $routeStatus = "OK_ENFORCED"; + } + + # enforce by priority + if ( + ( + $routeStatus eq "USER_DISABLED" + || $routeStatus eq "USER_ABSENT" + || $routeStatus eq "USER_ASLEEP" + ) + && $loopPriority >= $prioThresGwEmg + ) + { + $routeStatus = "OK_EMERGENCY"; + } + + # add location status + if ( $useLocation == 2 ) { + $routeStatus .= "+LOCATION-UNAVAILABLE"; + } + elsif ( $useLocation == 1 ) { + $routeStatus .= "+LOCATION"; + } + + # # add to queue + # if ( + # ( + # $routeStatus eq "USER_DISABLED" + # || $routeStatus eq "USER_ABSENT" + # || $routeStatus eq "USER_ASLEEP" + # ) + # && $routeStatus !~ /^OK/ + # && !$softFail + # ) + # { + # $routeStatus .= "+QUEUE"; + # } + + my $gatewayType = + $type[$i] eq "mail" + ? "fhemMsgMail" + : GetType( $gatewayDev, "UNDEFINED" ); + + my $defTitle = ""; + $defTitle = + $cmdSchema->{ $type[$i] }{$gatewayType} + {defaultValues}{$priorityCat}{TITLE} + if ( + defined( + $cmdSchema->{ $type[$i] }{$gatewayType} + {defaultValues}{$priorityCat}{TITLE} + ) + && $priorityCat ne "" + ); + $defTitle = + $cmdSchema->{ $type[$i] }{$gatewayType} + {defaultValues}{Normal}{TITLE} + if ( + defined( + $cmdSchema->{ $type[$i] }{$gatewayType} + {defaultValues}{Normal}{TITLE} + ) + && $priorityCat eq "" + ); + + Log3 $logDevice, 5, + "msg $device: " + . "Determined default title: $defTitle" + unless ( defined($testMode) && $testMode eq "1" ); + + # use title from device, global or internal default + my $loopTitle = $title; + $loopTitle = MSG_FindAttrVal( + $device, + "msgTitle$typeUc$priorityCat", + $typeUc, + MSG_FindAttrVal( + $device, + "msgTitle$typeUc", + $typeUc, + MSG_FindAttrVal( + $device, "msgTitle", + $typeUc, $defTitle + ) + ) + ) if ( $title eq "-" ); + + $loopTitle = "" + if ( $loopTitle eq "none" + || $loopTitle eq "-" ); + + my $loopMsg = $msg; + if ( $catchall == 1 && $type[$i] ne "queue" ) { + $loopTitle = "Fw: $loopTitle" + if ( $loopTitle ne "" + && $type[$i] !~ /^(audio|screen)$/ ); + $loopMsg = "Forwarded Message: $loopMsg" + if ( $loopTitle eq "" ); + if ( $type[$i] eq "mail" ) { + $loopMsg .= + "\n\n-- \nMail catched " + . "from device $device"; + } + elsif ( $type[$i] !~ /^(audio|screen)$/ ) { + $loopMsg .= + " ### (Catched from device $device)"; + } + } + + my $loopMsgShrt = + defined( $params->{msgTextShrt} ) + ? $params->{msgTextShrt} + : $msg; + + # correct message format + # + + # Remove Sonos Speak commands + $loopMsg =~ s/(\s*\|\w+\|\s*)/\\n\\n/gi + if ( $type[$i] ne "audio" ); + $loopMsgShrt =~ s/(\s*\|\w+\|\s*)/\\n\\n/gi + if ( $type[$i] ne "audio" ); + + # Replace new line with HTML break + # for e-mails + $loopMsg =~ s/\n/<br \/>\n/gi + if ( $type[$i] eq "mail" ); + $loopMsgShrt =~ s/\n/<br \/>\n/gi + if ( $type[$i] eq "mail" ); + + # use command from device, global or internal default + my $defCmd = ""; + $defCmd = + $cmdSchema->{ $type[$i] }{$gatewayType} + {$priorityCat} + if ( + defined( + $cmdSchema->{ $type[$i] }{$gatewayType} + {$priorityCat} + ) + && $priorityCat ne "" + ); + $defCmd = + $cmdSchema->{ $type[$i] }{$gatewayType}{Normal} + if ( + defined( + $cmdSchema->{ $type[$i] }{$gatewayType} + {Normal} + ) + && $priorityCat eq "" + ); + my $cmd = + + # gateway device + AttrVal( + $gatewayDev, "msgCmd$typeUc$priorityCat", + + MSG_FindAttrVal( + $device, "msgCmd$typeUc$priorityCat", + $typeUc, $defCmd + ) + ); + + if ( $cmd eq "" && $type[$i] ne "queue" ) { + Log3 $logDevice, 4, + "$gatewayDev: Unknown command schema " + . "for gateway device type $gatewayType. Use manual definition by userattr msgCmd*" + unless ( defined($testMode) + && $testMode eq "1" ); + $loopReturn3 .= + "$gatewayDev: Unknown command schema " + . "for gateway device type $gatewayType. Use manual definition by userattr msgCmd*\n"; + next; + } + + # ReplaceSetMagic + # + my $replaceError; + if ( $featurelevel >= 5.7 ) { + my %dummy; + my ( $err, @a ); + + # TITLE + ( $err, @a ) = + ReplaceSetMagic( \%dummy, 0, ($loopTitle) ); + $replaceError .= + "ReplaceSetMagic failed for TITLE: $err\n" + if ($err); + $loopTitle = join( " ", @a ) + unless ($err); + + # RECIPIENT + if ( $subRecipient ne "" ) { + ( $err, @a ) = + ReplaceSetMagic( \%dummy, 0, + ($subRecipient) ); + $replaceError .= + "ReplaceSetMagic failed " + . "for RECIPIENT: $err\n" + if ($err); + $subRecipient = join( " ", @a ) + unless ($err); + } + + # TERMINAL + if ( $termRecipient ne "" ) { + ( $err, @a ) = + ReplaceSetMagic( \%dummy, 0, + ($termRecipient) ); + $replaceError .= + "ReplaceSetMagic failed " + . "for TERMINAL: $err\n" + if ($err); + $termRecipient = join( " ", @a ) + unless ($err); + } + } + + $cmd =~ s/%PRIORITY%/$loopPriority/gi; + $cmd =~ s/%PRIOCAT%/$priorityCat/gi; + $cmd =~ s/%MSG%/$loopMsg/gi; + $cmd =~ s/%MSGSHRT%/$loopMsgShrt/gi; + $cmd =~ s/%MSGID%/$msgID.$sentCounter/gi; + $cmd =~ s/%TITLE%/$loopTitle/gi; + + my $loopTitleShrt = + defined( $params->{msgTitleShrt} ) + ? $params->{msgTitleShrt} + : MSG_FindAttrVal( + $device, + "msgTitleShrt$typeUc$priorityCat", + $typeUc, + MSG_FindAttrVal( + $device, + "msgTitleShrt$typeUc", + $typeUc, + MSG_FindAttrVal( + $device, "msgTitleShrt", + $typeUc, $loopTitle + ) + ) + ); + + $loopTitleShrt = + substr( $loopTitleShrt, 0, 37 ) . "..." + if ( length($loopTitleShrt) > 40 ); + $cmd =~ s/%TITLESHRT%/$loopTitleShrt/gi; + $loopTitleShrt =~ s/ /_/; + $cmd =~ s/%TITLESHRT2%/$loopTitleShrt/gi; + $loopTitleShrt =~ s/^([\s\t ]*\w+).*/$1/g; + $loopTitleShrt = + substr( $loopTitleShrt, 0, 17 ) . "..." + if ( length($loopTitleShrt) > 20 ); + $cmd =~ s/%TITLESHRT3%/$loopTitleShrt/gi; + + my $deviceName = AttrVal( + $device, + AttrVal( + $device, + "rg_realname", + AttrVal( $device, "rr_realname", "group" ) + ), + AttrVal( $device, "alias", $device ) + ); + my $deviceName2 = $deviceName; + $deviceName2 =~ s/ /_/; + + $cmd =~ s/%SOURCE%/$device/gi; + $cmd =~ s/%SRCALIAS%/$deviceName/gi; + $cmd =~ s/%SRCALIAS2%/$deviceName2/gi; + + my $gatewayDevName = AttrVal( + $gatewayDev, + AttrVal( + $gatewayDev, + "rg_realname", + AttrVal( + $gatewayDev, "rr_realname", "group" + ) + ), + AttrVal( $gatewayDev, "alias", $gatewayDev ) + ); + my $gatewayDevName2 = $gatewayDevName; + $gatewayDevName2 =~ s/ /_/; + + $cmd =~ s/%DEVICE%/$gatewayDev/gi; + $cmd =~ s/%DEVALIAS%/$gatewayDevName/gi; + $cmd =~ s/%DEVALIAS2%/$gatewayDevName2/gi; + + my $loopMsgDateTime = $msgDateTime; + $loopMsgDateTime .= ".$sentCounter" + if ($sentCounter); + my $loopMsgDateTime2 = $loopMsgDateTime; + $loopMsgDateTime2 =~ s/ /_/; + + $cmd =~ s/%MSGDATETIME%/$loopMsgDateTime/gi; + $cmd =~ s/%MSGDATETIME2%/$loopMsgDateTime2/gi; + + my $subRecipientName = + $subRecipient eq "" + ? "" + : AttrVal( + $subRecipient, + AttrVal( + $subRecipient, + "rg_realname", + AttrVal( + $subRecipient, "rr_realname", + "group" + ) + ), + AttrVal( + $subRecipient, "alias", $subRecipient + ) + ); + my $subRecipientName2 = $subRecipientName; + $subRecipientName2 =~ s/ /_/; + + $cmd =~ s/%RECIPIENT%/$subRecipient/gi + if ( $subRecipient ne "" ); + $cmd =~ s/%RCPTNAME%/$subRecipientName/gi + if ( $subRecipientName ne "" ); + $cmd =~ s/%RCPTNAME2%/$subRecipientName2/gi + if ( $subRecipientName2 ne "" ); + $cmd =~ s/%TERMINAL%/$termRecipient/gi + if ( $termRecipient ne "" ); + + my $paramsA; + + unless ( defined($testMode) && $testMode eq "1" ) { + + # user parameters from message + if ( ref($params) eq "HASH" ) { + for my $key ( keys %$params ) { + next if ( ref( $params->{$key} ) ); + my $val = $params->{$key}; + $cmd =~ s/%$key%/$val/gi; + $cmd =~ s/\$$key/$val/g; + Log3 $logDevice, 5, + "msg $device: User parameters: " + . "replacing %$key% and \$$key by '$val'"; + } + } + + # user parameters from attributes + my $paramsAttr1 = + AttrVal( $gatewayDev, + "msgParams$typeUc$priorityCat", undef ); + my $paramsAttr2 = + AttrVal( $gatewayDev, "msgParams$typeUc", + undef ); + my $paramsAttr3 = + AttrVal( $gatewayDev, "msgParams", undef ); + my $paramsAttr4 = + MSG_FindAttrVal( $device, + "msgParams$typeUc$priorityCat", + $typeUc, undef ); + my $paramsAttr5 = + MSG_FindAttrVal( $device, "msgParams$typeUc", + $typeUc, undef ); + my $paramsAttr6 = + MSG_FindAttrVal( $device, "msgParams", + $typeUc, undef ); + + foreach ( + $paramsAttr1, $paramsAttr2, $paramsAttr3, + $paramsAttr4, $paramsAttr5, $paramsAttr6 + ) + { + next unless ($_); + if ( $_ =~ m/^{.*}$/s + && $_ =~ m/=>/ + && $_ !~ m/\$/ ) + { + my $av = eval $_; + if ($@) { + Log3 $logDevice, 3, + "msg $device: " + . "ERROR while reading attribute msgParams"; + } + else { + $paramsA = $av; + } + } + else { + my ( $a, $h ) = parseParams($_); + $paramsA = $h; + } + + next unless ref($paramsA) eq "HASH"; + + if ( ref($paramsA) eq "HASH" ) { + for my $key ( keys %$paramsA ) { + next if ( ref( $params->{$key} ) ); + my $val = $paramsA->{$key}; + $cmd =~ s/%$key%/$val/gi; + $cmd =~ s/\$$key/$val/g; + Log3 $logDevice, 5, + "msg $device: " + . "msgParams: replacing %$key% and \$$key by '$val'"; + } + } + } + + # user parameters from command schema hash + if ( + $priorityCat ne "" + && defined( + $cmdSchema->{ $type[$i] }{$gatewayType} + {defaultValues}{$priorityCat} + ) + ) + { + + for my $item ( + $cmdSchema->{ $type[$i] }{$gatewayType} + {defaultValues}{$priorityCat} ) + { + for my $key ( keys(%$item) ) { + my $val = $item->{$key}; + $cmd =~ s/%$key%/$val/gi; + $cmd =~ s/\$$key/$val/g; + Log3 $logDevice, 5, + "msg $device: " + . "msgSchema: replacing %$key% and \$$key by '$val'"; + } + } + + } + elsif ( + $priorityCat eq "" + && defined( + $cmdSchema->{ $type[$i] }{$gatewayType} + {defaultValues}{Normal} + ) + ) + { + + for my $item ( + $cmdSchema->{ $type[$i] }{$gatewayType} + {defaultValues}{Normal} ) + { + for my $key ( keys(%$item) ) { + my $val = $item->{$key}; + $cmd =~ s/%$key%/$val/gi; + $cmd =~ s/\$$key/$val/g; + Log3 $logDevice, 5, + "msg $device: " + . "msgSchema: replacing %$key% and \$$key by '$val'"; + } + } + + } + + } + + $sentCounter++; + + if ( $routeStatus =~ /^OK\w*/ ) { + + my $error = 0; + + unless ( defined($testMode) + && $testMode eq "1" ) + { + + # ReplaceSetMagic + # + if ( $featurelevel >= 5.7 + && !$replaceError ) + { + my %dummy; + my ( $err, @a ) = + ReplaceSetMagic( \%dummy, 0, ($cmd) ); + $replaceError .= + "ReplaceSetMagic failed for CMD: " + . "$err\n" + if ($err); + $cmd = join( " ", @a ) + unless ($err); + } + + # add user parameters + # if gateway supports parseParams + my $gatewayDevType = GetType($gatewayDev); + if ( + $gatewayDevType + && ref($params) eq "HASH" + && ( $modules{$gatewayDevType} + ->{parseParams} + || $modules{$gatewayDevType} + ->{msgParams}{parseParams} + || $modules{$gatewayDevType} + ->{'.msgParams'}{parseParams} ) + ) + { + Log3 $logDevice, 5, + "msg $device: " + . "parseParams support: Handing over user parameters to other device"; + + my ( $a, $h ) = parseParams($cmd); + + keys %$params; + while ( ( my $key, my $value ) = + each %$params ) + { + # Compatibility to legacy schema: + # lowercase after _ + my $s = $gatewayDevType + . "[\_\/-]([A-Z0-9_-]+)"; + $key =~ s/^$s$/\L$1/; + + # remove gateway TYPE when + # used as prefix + $s = $gatewayDevType . "[_\/-]"; + $key =~ s/^$s//; + $cmd .= " $key='$value'" + if ( !defined( $h->{$key} ) + || $h->{$key} =~ + m/^[\s\t\n ]*$/ ); + } + + keys %$paramsA; + while ( ( my $key, my $value ) = + each %$paramsA ) + { + # Compatibility to legacy schema: + # lowercase after _ + my $s = $gatewayDevType + . "[\_\/-]([A-Z0-9_-]+)"; + $key =~ s/^$s$/\L$1/; + + # remove gateway TYPE when + # used as prefix + $s = $gatewayDevType . "[_\/-]"; + $key =~ s/^$s//; + $cmd .= " $key='$value'" + if ( !defined( $h->{$key} ) + || $h->{$key} =~ + m/^[\s\t\n ]*$/ ); + } + + } + + # excplicitly queue message + if ( $routeStatus eq "OK_QUEUE" ) { + $queued = msgConfig_QueueAdd( + $msgA, $params, + $msgDateTime, $msgID, + $sentCounter, $type[$i], + $device, $subRecipient, + $termRecipient, $priority, + $loopTitle, $loopMsg + ) ? 1 : 0; + } + + # run command + elsif ($replaceError) { + $error = 2; + $loopReturn3 .= $replaceError; + } + elsif ( $cmd =~ /^\s*\{.*\}\s*$/ ) { + Log3 $logDevice, 5, + "msg $device: " + . "$type[$i] route command (Perl): $cmd"; + #eval $cmd; + my $ret = AnalyzePerlCommand(undef, $cmd); + unless ( !$ret || $ret =~ m/^[\s\t\n ]*$/ ) + { + $error = 1; + $loopReturn3 .= "$gatewayDev: $ret\n"; + } + } + else { + Log3 $logDevice, 5, + "msg $device: " + . "$type[$i] route command (fhem): $cmd"; + my $ret = AnalyzeCommandChain(undef,$cmd); + unless ( !$ret + || $ret =~ m/^[\s\t\n ]*$/ ) + { + $error = 1; + $loopReturn3 .= + "$gatewayDev: $ret\n"; + } + } + + $routeStatus = "ERROR" + if ( $error == 1 ); + $routeStatus = "ERROR_EVAL" + if ( $error == 2 ); + + Log3 $logDevice, 3, + "msg $device: " + . "ID=$msgID.$sentCounter " + . "TYPE=$type[$i] " + . "ROUTE=$gatewayDev " + . "RECIPIENT=$subRecipient " + . "STATUS=$routeStatus " + . "PRIORITY=$loopPriority($priorityCat) " + . "TITLE='$loopTitle' " + . "MSG='$loopMsg'" + if ( $priorityCat ne "" + && $subRecipient ne "" ); + Log3 $logDevice, 3, + "msg $device: " + . "ID=$msgID.$sentCounter " + . "TYPE=$type[$i] " + . "ROUTE=$gatewayDev " + . "RECIPIENT=$subRecipient " + . "STATUS=$routeStatus " + . "PRIORITY=$loopPriority " + . "TITLE='$loopTitle' " + . "MSG='$loopMsg'" + if ( $priorityCat eq "" + && $subRecipient ne "" ); + Log3 $logDevice, 3, + "msg $device: " + . "ID=$msgID.$sentCounter " + . "TYPE=$type[$i] " + . "ROUTE=$gatewayDev " + . "STATUS=$routeStatus " + . "PRIORITY=$loopPriority($priorityCat) " + . "TITLE='$loopTitle' " + . "MSG='$loopMsg'" + if ( $priorityCat ne "" + && $subRecipient eq "" ); + Log3 $logDevice, 3, + "msg $device: " + . "ID=$msgID.$sentCounter " + . "TYPE=$type[$i] " + . "ROUTE=$gatewayDev " + . "STATUS=$routeStatus " + . "PRIORITY=$loopPriority " + . "TITLE='$loopTitle' " + . "MSG='$loopMsg'" + if ( $priorityCat eq "" + && $subRecipient eq "" ); + + } + + $msgSent = 1 if ( $error == 0 ); + $msgSentDev = 1 if ( $error == 0 ); + + if ( $subRecipient ne "" ) { + $gatewaysStatus{"$gatewayDev:$subRecipient"} + = $routeStatus + if ( $globalDevName ne $gatewayDev ); + $gatewaysStatus{"$device:$subRecipient"} = + $routeStatus + if ( $globalDevName eq $gatewayDev ); + } + else { + $gatewaysStatus{$gatewayDev} = $routeStatus + if ( $globalDevName ne $gatewayDev ); + $gatewaysStatus{$device} = $routeStatus + if ( $globalDevName eq $gatewayDev ); + } + } + + elsif ( $routeStatus =~ /\+QUEUE/ || $forceQueue ) { + unless ( defined($testMode) + && $testMode eq "1" ) + { + if ( !( grep { "queue" eq $_ } @type ) ) { + $queued = msgConfig_QueueAdd( + $msgA, $params, + $msgDateTime, $msgID, + $sentCounter, $type[$i], + $device, $subRecipient, + $termRecipient, $priority, + $loopTitle, $loopMsg + ) ? 1 : 0; + } + + Log3 $logDevice, 3, + "msg $device: " + . "ID=$msgID.$sentCounter " + . "TYPE=$type[$i] " + . "ROUTE=$gatewayDev " + . "RECIPIENT=$subRecipient " + . "STATUS=$routeStatus " + . "PRIORITY=$loopPriority " + . "TITLE='$loopTitle' '$loopMsg'" + if ( $subRecipient ne "" ); + Log3 $logDevice, 3, + "msg $device: " + . "ID=$msgID.$sentCounter " + . "TYPE=$type[$i] " + . "ROUTE=$gatewayDev " + . "STATUS=$routeStatus " + . "PRIORITY=$loopPriority " + . "TITLE='$loopTitle' '$loopMsg'" + if ( $subRecipient eq "" ); + } + + $msgSent = 3 if ( $msgSent != 1 ); + $msgSentDev = 3 if ( $msgSentDev != 1 ); + + $gatewaysStatus{$gatewayDev} = $routeStatus + if ( $globalDevName ne $gatewayDev ); + $gatewaysStatus{$device} = $routeStatus + if ( $globalDevName eq $gatewayDev ); + } + + elsif ($routeStatus eq "UNAVAILABLE" + || $routeStatus eq "UNDEFINED" ) + { + unless ( defined($testMode) + && $testMode eq "1" ) + { + Log3 $logDevice, 3, + "msg $device: " + . "ID=$msgID.$sentCounter " + . "TYPE=$type[$i] " + . "ROUTE=$gatewayDev " + . "RECIPIENT=$subRecipient " + . "STATUS=$routeStatus " + . "PRIORITY=$loopPriority " + . "TITLE='$loopTitle' '$loopMsg'" + if ( $subRecipient ne "" ); + Log3 $logDevice, 3, + "msg $device: " + . "ID=$msgID.$sentCounter " + . "TYPE=$type[$i] " + . "ROUTE=$gatewayDev " + . "STATUS=$routeStatus " + . "PRIORITY=$loopPriority " + . "TITLE='$loopTitle' '$loopMsg'" + if ( $subRecipient eq "" ); + } + + $gatewaysStatus{$gatewayDev} = $routeStatus + if ( $globalDevName ne $gatewayDev ); + $gatewaysStatus{$device} = $routeStatus + if ( $globalDevName eq $gatewayDev ); + } + + else { + unless ( defined($testMode) + && $testMode eq "1" ) + { + Log3 $logDevice, 3, + "msg $device: " + . "ID=$msgID.$sentCounter " + . "TYPE=$type[$i] " + . "ROUTE=$gatewayDev " + . "RECIPIENT=$subRecipient " + . "STATUS=$routeStatus " + . "PRIORITY=$loopPriority " + . "TITLE='$loopTitle' '$loopMsg'" + if ( $subRecipient ne "" ); + Log3 $logDevice, 3, + "msg $device: " + . "ID=$msgID.$sentCounter " + . "TYPE=$type[$i] " + . "ROUTE=$gatewayDev " + . "STATUS=$routeStatus " + . "PRIORITY=$loopPriority " + . "TITLE='$loopTitle' '$loopMsg'" + if ( $subRecipient eq "" ); + } + + $msgSent = 2 if ( $msgSent != 1 ); + $msgSentDev = 2 if ( $msgSentDev != 1 ); + + $gatewaysStatus{$gatewayDev} = $routeStatus + if ( $globalDevName ne $gatewayDev ); + $gatewaysStatus{$device} = $routeStatus + if ( $globalDevName eq $gatewayDev ); + } + + } + + last if ( $msgSentDev == 1 || $msgSentDev == 3 ); + } + + ##################### + # return if we are in routing target test mode + # + if ( defined($testMode) && $testMode eq "1" ) { + Log3 $logDevice, 5, + "msg $device: " + . "$type[$i] route check result: ROUTE_AVAILABLE" + if ( $loopReturn3 eq "" ); + Log3 $logDevice, 5, + "msg $device: " + . "$type[$i] route check result: ROUTE_UNAVAILABLE" + if ( $loopReturn3 ne "" ); + return "ROUTE_AVAILABLE" if ( $loopReturn3 eq "" ); + return "ROUTE_UNAVAILABLE" if ( $loopReturn3 ne "" ); + } + + if ( $catchall == 0 ) { + if ( !defined( $sentTypesPerDevice{$device} ) ) { + $sentTypesPerDevice{$device} = ""; + } + else { + $sentTypesPerDevice{$device} .= " "; + } + + $sentTypesPerDevice{$device} .= + $type[$i] . ":" . $msgSentDev; + } + else { + if ( !defined( $sentTypesPerDevice{$device} ) ) { + $sentTypesPerDevice{$globalDevName} = ""; + } + else { + $sentTypesPerDevice{$globalDevName} .= " "; + } + + $sentTypesPerDevice{$globalDevName} .= + $type[$i] . ":" . $msgSentDev; + } + + # update device readings + my $readingsDev = $defs{$device}; + $readingsDev = $defs{$globalDevName} + if ( $catchall == 1 || $deviceType eq "email" ); + readingsBeginUpdate($readingsDev); + + readingsBulkUpdate( $readingsDev, "fhemMsg" . $typeUc, + $msg ); + readingsBulkUpdate( $readingsDev, + "fhemMsg" . $typeUc . "Title", $title ); + readingsBulkUpdate( $readingsDev, + "fhemMsg" . $typeUc . "Prio", + $loopPriority ); + + my $gwStates = "-"; + + keys %gatewaysStatus; + while ( ( my $gwName, my $gwState ) = each %gatewaysStatus ) + { + $gwStates = "" if $gwStates eq "-"; + $gwStates .= " " if $gwStates ne "-"; + $gwStates .= "$gwName:$gwState"; + } + readingsBulkUpdate( $readingsDev, + "fhemMsg" . $typeUc . "Gw", $gwStates ); + readingsBulkUpdate( $readingsDev, + "fhemMsg" . $typeUc . "State", $msgSentDev ); + + # suppress errors when there are still alternatives + if ( $hasTypeOr == 1 + && $isTypeOr < scalar( grep { defined $_ } @typesOr ) ) + { + $loopReturn3 = ""; + } + + ################################################################ + ### Implicit forwards based on priority or presence + ### + + # Skip if typeOr is defined + # and this is not the last type entry + if ( $msgSentDev != 3 + && $msgSentDev != 1 + && $hasTypeOr == 1 + && $isTypeOr < scalar( grep { defined $_ } @typesOr ) ) + { + Log3 $logDevice, 4, + "msg $device: " + . "Skipping implicit forward due to typesOr definition"; + + # remove recipient from list to avoid + # other interaction when using recipientOr in parallel + if ( $hasRecipientOr == 1 + && $isRecipientOr < + scalar( grep { defined $_ } @recipientsOr ) ) + { + my $regex1 = + "\\s*!?@?" . $device . "[,|]"; # at the beginning + my $regex2 = + "[,|]!?@?" . $device . "\\s*"; # at the end + my $regex3 = + ",!?@?" + . $device + . ","; # in the middle with comma + my $regex4 = + "[\|,]!?@?" + . $device + . "[\|,]"; # in the middle with pipe and/or comma + + $recipients =~ s/^$regex1//; + $recipients =~ s/$regex2$/|/gi; + $recipients =~ s/$regex3/,/gi; + $recipients =~ s/$regex4/|/gi; + } + + } + + # Skip if recipientOr is defined + # and this is not the last device entry + if ( $msgSentDev != 3 + && $msgSentDev != 1 + && $hasRecipientOr == 1 + && $isRecipientOr < + scalar( grep { defined $_ } @recipientsOr ) ) + { + Log3 $logDevice, 4, + "msg $device: " + . "Skipping implicit forward due to recipientOr definition"; + } + + # Skip if softFail + elsif ( $msgSentDev != 3 && $msgSentDev != 1 && $softFail ) + { + Log3 $logDevice, 4, + "msg $device: Skipping implicit forward"; + $loopReturn3 = ""; + } + + # priority forward thresholds + # + + ### emergency + my $msgFwPrioEmergency = + MSG_FindAttrVal( $device, "msgFwPrioEmergency$typeUc", + $typeUc, 2 ); + + ### absent + my $msgFwPrioAbsent = + MSG_FindAttrVal( $device, "msgFwPrioAbsent$typeUc", + $typeUc, 0 ); + + ### gone + my $msgFwPrioGone = + MSG_FindAttrVal( $device, "msgFwPrioGone$typeUc", + $typeUc, 1 ); + + my $fw_gwUnavailable = + defined( + $settings->{ $type[$i] }{typeEscalation}{gwUnavailable} + ) + ? $settings->{ $type[$i] }{typeEscalation}{gwUnavailable} + : ""; + my $fw_emergency = + defined( + $settings->{ $type[$i] }{typeEscalation}{emergency} ) + ? $settings->{ $type[$i] }{typeEscalation}{emergency} + : ""; + my $fw_residentAbsent = + defined( + $settings->{ $type[$i] }{typeEscalation}{residentAbsent} + ) + ? $settings->{ $type[$i] }{typeEscalation}{residentAbsent} + : ""; + my $fw_residentGone = + defined( + $settings->{ $type[$i] }{typeEscalation}{residentGone} ) + ? $settings->{ $type[$i] }{typeEscalation}{residentGone} + : ""; + + # Forward message + # if no gateway device for this type was available + if ( $msgSentDev == 0 + && $fw_gwUnavailable ne "" + && !( grep { $fw_gwUnavailable eq $_ } @type ) + && $routes{$fw_gwUnavailable} == 1 ) + { + Log3 $logDevice, 4, + "msg $device: " + . "Implicit forwards: No $type[$i] gateway device available for recipient $device ($gatewayDevs). Trying alternative message type " + . $fw_gwUnavailable; + + push @type, $fw_gwUnavailable; + $forwarded .= "," if ( $forwarded ne "" ); + $forwarded .= $type[$i] . ">" . $fw_gwUnavailable; + } + + # Forward message + # if emergency priority + if ( $loopPriority >= $msgFwPrioEmergency + && $fw_emergency ne "" + && !( grep { $fw_emergency eq $_ } @type ) + && $routes{$fw_emergency} == 1 ) + { + Log3 $logDevice, 4, + "msg $device: " + . "Implicit forwards: Escalating high priority $type[$i] message via " + . $fw_emergency; + + push @type, $fw_emergency; + $forwarded .= "," if ( $forwarded ne "" ); + $forwarded .= $type[$i] . ">" . $fw_emergency; + } + + # Forward message + # if high priority and residents are + # constantly not at home + if ( $residentDevPresence eq "absent" + && $loopPriority >= $msgFwPrioGone + && $fw_residentGone ne "" + && !( grep { $fw_residentGone eq $_ } @type ) + && $routes{$fw_residentGone} == 1 ) + { + Log3 $logDevice, 4, + "msg $device: " + . "Implicit forwards: Escalating high priority $type[$i] message via " + . $fw_residentGone; + + push @type, $fw_residentGone; + $forwarded .= "," if ( $forwarded ne "" ); + $forwarded .= $type[$i] . ">" . $fw_residentGone; + } + + # Forward message + # if priority is normal or higher and residents + # are not at home but nearby + if ( !$forceQueue + && $residentDevState eq "absent" + && $loopPriority >= $msgFwPrioAbsent + && $fw_residentAbsent ne "" + && !( grep { $fw_residentAbsent eq $_ } @type ) + && $routes{$fw_residentAbsent} == 1 ) + { + Log3 $logDevice, 4, + "msg $device: " + . "Implicit forwards: Escalating $type[$i] message via " + . $fw_residentAbsent + . " due to absence"; + + push @type, $fw_residentAbsent; + $forwarded .= "," if ( $forwarded ne "" ); + $forwarded .= $type[$i] . ">" . $fw_residentAbsent; + } + + } + + $loopReturn2 .= $loopReturn3 unless ($softFail); + last if ( $msgSent == 1 ); + + $isTypeOr++; + } + + $loopReturn1 .= $loopReturn2; + } + + $return .= $loopReturn1; + last if ( $msgSent == 1 ); + + $isRecipientOr++; + } + + # finalize device readings + keys %sentTypesPerDevice; + while ( ( my $device, my $types ) = each %sentTypesPerDevice ) { + $device = $globalDevName + if ( $device =~ /^(([A-Za-z0-9%+._-])+@+([%+a-z0-9A-Z.-]*))$/ ); + + readingsBulkUpdate( $defs{$device}, "fhemMsgStateTypes", $types ) + if ( $forwarded eq "" ); + readingsBulkUpdate( $defs{$device}, "fhemMsgStateTypes", + $types . " forwards:" . $forwarded ) + if ( $forwarded ne "" ); + readingsBulkUpdate( $defs{$device}, "fhemMsgState", $msgSent ); + readingsEndUpdate( $defs{$device}, 1 ); + } + + $return .= "However, message was still sent to some recipients!" + if ( $msgSent == 1 && $return ne "" ); + + $return .= + "FATAL ERROR: Message NOT sent. No gateway device was available." + if ( !$softFail && $msgSent == 2 ); + + return $return; +} + +1; + +=pod +=item command +=item summary dynamic routing of messages to FHEM devices and modules +=item summary_DE dynamisches Routing für Nachrichten an FHEM Geräte und Module +=begin html + +<a name="MSG"></a> +<h3>msg</h3> +<ul> + <code>msg [<type>] [<@device>|<e-mail address>] [<priority>] [|<title>|] <message></code> + <br> + <br> + No documentation here yet, sorry.<br> + <a href="http://forum.fhem.de/index.php/topic,39983.0.html">FHEM Forum</a> +</ul> + +=end html +=begin html_DE + +<a name="MSG"></a> +<h3>msg</h3> +<ul> + <code>msg [<type>] [<@device>|<e-mail address>] [<priority>] [|<title>|] <message></code> + <br> + <br> + Bisher keine Dokumentation hier, sorry.<br> + <a href="http://forum.fhem.de/index.php/topic,39983.0.html">FHEM Forum</a> +</ul> + + +=end html_DE + +=for :application/json;q=META.json 75_MSG.pm +{ + "author": [ + "Julian Pawlowski <julian.pawlowski@gmail.com>" + ], + "x_fhem_maintainer": [ + "loredo" + ], + "x_fhem_maintainer_github": [ + "jpawlowski" + ], + "keywords": [ + "audio router", + "email", + "messaging", + "messenger", + "push" + ] +} +=end :application/json;q=META.json + +=cut diff --git a/README.md b/README.md new file mode 100644 index 0000000..ddca5ad --- /dev/null +++ b/README.md @@ -0,0 +1,17 @@ +# FHEM::MSG + +## Used branching model +* Master branch: Production version (copy of [fhem/fhem-mirror/blob/master/fhem/FHEM/75_MSG.pm](https://github.com/fhem/fhem-mirror/blob/master/fhem/FHEM/75_MSG.pm)) +* Dev branch: Latest development version + +## Community support +The FHEM Forum is available [here](https://forum.fhem.de/) for general support. +In case you have a specific question about this command, it is recommended to find the right sub-forum. +It can either be found from the command info card using the FHEM Installer (e.g. using command `search <MODULE_NAME>`) or it can be determine from the [MAINTAINER.txt](https://github.com/fhem/fhem-mirror/blob/master/fhem/MAINTAINER.txt) file. + +## Bug reports and feature requests +Identified bugs and feature requests are tracked using [Github Issues](https://github.com/fhem/MSG/issues). + +## Pull requests / How to participate into development +You are invited to send pull requests to the dev branch whenever you think you can contribute with some useful improvements to the command. +The command maintainer will review you code and decide wether it is going to be part of the command in a future release.