diff --git a/fhem/contrib/98_gcmsend.pm b/fhem/contrib/98_gcmsend.pm index 7dcc85cf8..c07a9ba49 100644 --- a/fhem/contrib/98_gcmsend.pm +++ b/fhem/contrib/98_gcmsend.pm @@ -1,282 +1,332 @@ package main; +use strict; +use warnings; use HTTP::Request; use LWP::UserAgent; use IO::Socket::SSL; use utf8; +use Crypt::CBC; +use Crypt::Cipher::AES; -my @gets = ('dummy'); - -sub -gcmsend_Initialize($) +sub gcmsend_Initialize($) { - my ($hash) = @_; - $hash->{DefFn} = "gcmsend_Define"; - $hash->{NotifyFn} = "gcmsend_notify"; - $hash->{SetFn} = "gcmsend_set"; - $hash->{AttrList} = "loglevel:0,1,2,3,4,5 regIds apiKey stateFilter vibrate deviceFilter playSound"; + my ($hash) = @_; + $hash->{DefFn} = "gcmsend_Define"; + $hash->{NotifyFn} = "gcmsend_notify"; + $hash->{AttrFn} = "gcmsend_attr"; + $hash->{SetFn} = "gcmsend_set"; + $hash->{AttrList} = "loglevel:0,1,2,3,4,5 regIds apiKey stateFilter vibrate deviceFilter cryptKey"; } -sub -gcmsend_set { - my ($hash, @a) = @_; - my $v = @a[1]; - if ($v eq "delete_saved_states") { - $hash->{STATES} = {}; - return "deleted"; - } elsif($v eq "send") { - my $msg = ""; - for (my $i = 2; $i < int(@a); $i++) { - if (! ($msg eq "")) { - $msg .= " "; - } - $msg .= @a[$i]; +sub gcmsend_attr { + my ($cmd, $name, $aName, $aVal) = @_; + if (not $aName eq "cryptKey") { + return undef; } - return gcmsend_sendMessage($hash, $msg); - } else { - return "unknown set value, choose one of delete_saved_states send"; - } + $aVal = sprintf("%016s", $aVal); + $aVal = substr $aVal, length($aVal) - 16, 16; + $_[3] = $aVal; + return undef; } -sub -gcmsend_Define($$) +sub gcmsend_set { + my ($hash, @a) = @_; + my $v = @a[1]; + if ($v eq "delete_saved_states") { + $hash->{STATES} = { }; + return "deleted"; + } elsif ($v eq "send") { + my $msg = ""; + for (my $i = 2; $i < int(@a); $i++) { + if (!($msg eq "")) { + $msg .= " "; + } + $msg .= @a[$i]; + } + return gcmsend_sendMessage($hash, $msg); + } else { + return "unknown set value, choose one of delete_saved_states send"; + } +} + +sub gcmsend_Define($$) { - my ($hash, $def) = @_; + my ($hash, $def) = @_; - my @args = split("[ \t]+", $def); + my @args = split("[ \t]+", $def); - if (int(@args) < 1) - { - return "gcmsend_Define: too many arguments. Usage:\n" . - "define gcmsend"; - } - return "Invalid arguments. Usage: \n define gcmsend" if(int(@a) != 0); - - $hash->{STATE} = 'Initialized'; + if (int(@args) < 1) + { + return "gcmsend_Define: too many arguments. Usage:\n". + "define gcmsend"; + } + return "Invalid arguments. Usage: \n define gcmsend" if (int(@args) != 2); - return undef; + $hash->{STATE} = 'Initialized'; + + return undef; } sub gcmsend_array_to_json(@) { - my (@array) = @_; - my $ret = ""; + my (@array) = @_; + my $ret = ""; - for (my $i = 0; $i < int(@array); $i++) { - if ($i != 0) { - $ret .= ","; + for (my $i = 0; $i < int(@array); $i++) { + if ($i != 0) { + $ret .= ","; + } + my $value = @array[$i]; + $ret .= ("\"".$value."\""); } - my $value = @array[$i]; - $ret .= ("\"" . $value . "\""); - } - - return "[" . $ret . "]"; + + return "[".$ret."]"; } -sub gcmsend_sendPayload($$) { - my ($hash, $payload) = @_; +sub gcmsend_sendPayload($%) { + my ($hash, %payload) = @_; + my %generalPayload = gcmsend_getGeneralPayload($hash); + my %toSendPayload = (%generalPayload, %payload); + my %encryptedPayload = gcmsend_encrypt($hash, %toSendPayload); - my $name = $hash->{NAME}; - - my $logLevel = GetLogLevel($name,5); - - my $client = LWP::UserAgent->new(); - my $regIdsText = AttrVal($name, "regIds", ""); - - my $apikey = AttrVal($name, "apiKey", ""); - my @registrationIds = split(/\|/, $regIdsText); - - if (int(@registrationIds) == 0) { - Log $logLevel, "$name no registrationIds set."; - return undef; - } - return undef if (int(@registrationIds) == 0); - - my $unixTtimestamp = time*1000; + my $jsonPayload = gcmsend_toJson(%encryptedPayload); - my $data = - "{" . - "\"registration_ids\":" . gcmsend_array_to_json(@registrationIds) . "," . - "\"data\": $payload". - "}"; - - Log $logLevel, "data is $payload"; + my $name = $hash->{NAME}; - my $req = HTTP::Request->new(POST => "https://android.googleapis.com/gcm/send"); - $req->header(Authorization => 'key='.$apikey); - $req->header('Content-Type' => 'application/json; charset=UTF-8'); - $req->content($data); + my $logLevel = GetLogLevel($name, 5); - my $response = $client->request($req); - if (! $response->is_success) { - Log $logLevel, "error during request: " . $response->status_line; - $hash->{STATE} = $response->status_line; - } - $hash->{STATE} = "OK"; - return undef; + my $client = LWP::UserAgent->new(); + my $regIdsText = AttrVal($name, "regIds", ""); + + my $apikey = AttrVal($name, "apiKey", ""); + my @registrationIds = split(/\|/, $regIdsText); + + if (int(@registrationIds) == 0) { + Log $logLevel, "$name no registrationIds set."; + return undef; + } + return undef if (int(@registrationIds) == 0); + + my $data = + "{". + "\"registration_ids\":".gcmsend_array_to_json(@registrationIds).",". + "\"data\": $jsonPayload". + "}"; + + Log $logLevel, "data is $jsonPayload"; + + my $req = HTTP::Request->new( POST => "https://android.googleapis.com/gcm/send" ); + $req->header( Authorization => 'key='.$apikey ); + $req->header( 'Content-Type' => 'application/json; charset=UTF-8' ); + $req->content( $data ); + + my $response = $client->request( $req ); + if (!$response->is_success) { + Log $logLevel, "error during request: ".$response->status_line; + $hash->{STATE} = $response->status_line; + } + $hash->{STATE} = "OK"; + return undef; } -sub gcmsend_fillGeneralPayload($$) { - my ($hash, $payloadString) = @_; - - my $name = $hash->{NAME}; - - my $vibrate = "false"; - if (AttrVal($name, "vibrate", "false") eq "true") { - $vibrate = "true"; - } - my $playSound = "false"; - if (AttrVal($name, "playSound", "false") eq "true") { - $playSound = "true"; - } - - return $payloadString . "," . - "\"source\":\"gcmsend_fhem\"," . - "\"vibrate\":\"$vibrate\"," . - "\"playSound\":\"$playSound\""; +sub gcmsend_getGeneralPayload($) { + my ($hash) = @_; + + my $name = $hash->{NAME}; + + my $vibrate = "false"; + if (AttrVal($name, "vibrate", "false") eq "true") { + $vibrate = "true"; + } + + my $gcmName = $hash->{NAME}; + + my %generalPayload = ( + "source" => "gcmsend_fhem", + "gcmDeviceName" => $gcmName, + "vibrate" => "$vibrate" + ); + return %generalPayload; } sub gcmsend_sendNotify($$$) { - my ($hash, $deviceName, $changes) = @_; - - my $payload = - "\"deviceName\": \"$deviceName\"," . - "\"changes\":\"$changes\"," . - "\"type\":\"notify\""; - - $payload = "{" . gcmsend_fillGeneralPayload($hash, $payload) . "}"; - - gcmsend_sendPayload($hash, $payload); + my ($hash, $deviceName, $changes) = @_; + my %payload = ( + "deviceName" => $deviceName, + "changes" => $changes, + "type" => "notify" + ); + gcmsend_sendPayload($hash, %payload); +} + +sub gcmsend_toJson(%) { + my (%hash) = @_; + my @entries = (); + + while (my ($key, $value) = each %hash) { + my $entry = "\"$key\":\"$value\""; + push @entries, $entry; + } + return "{".join(", ", @entries)."}"; +} + +my %gcmsend_encrypt_keys = ("type" => "", "notifyId" => "", "changes" => "", "deviceName" => "", + "tickerText" => "", "contentText" => "", "contentTitle" => ""); +sub gcmsend_encrypt($%) { + my ($hash, %payload) = @_; + my $key = AttrVal($hash->{NAME}, "cryptKey", ""); + if ($key eq "") { + return %payload; + } + + my $cipher = Crypt::CBC->new( + -cipher => 'Crypt::Cipher::AES', + -key => $key, + -iv => $key, + -padding => 'standard', + -header => 'none', + -blocksize => '16', + -literal_key => 1, + -keysize => 16 + ); + my %newPayload = (); + while (my ($key, $value) = each %payload) { + if (exists(%gcmsend_encrypt_keys->{$key})) { + my $padded = sprintf '%16s', $value; + my $length = length($padded); + + %newPayload->{$key} = $cipher->encrypt_hex( $value ); + } else { + %newPayload->{$key} = $value; + } + } + return %newPayload; } sub gcmsend_sendMessage($$) { - my ($hash, $message) = @_; - - my @parts = split(/\|/, $message); - - my $tickerText; - my $contentTitle; - my $contentText; - my $notifyId = 1; - - my $length = int(@parts); - - if ($length == 3 || $length == 4) { - $tickerText = @parts[0]; - $contentTitle = @parts[1]; - $contentText = @parts[2]; - - if ($length == 4) { - my $notifyIdText = @parts[3]; - if (!(@parts[3] =~ m/[1-9][0-9]*/)) { - return "notifyId must be numeric and positive"; - } - $notifyId = @parts[3]; - } - } else { - return "Illegal message format. Required format is \r\n " . - "tickerText|contentTitle|contentText[|NotifyID]"; - } - - my $payload = - "\"tickerText\":\"$tickerText\"," . - "\"contentTitle\":\"$contentTitle\"," . - "\"contentText\":\"$contentText\"," . - "\"notifyId\":\"$notifyId\"," . - "\"source\":\"gcmsend_fhem\"," . - "\"type\":\"message\"" - ; + my ($hash, $message) = @_; - $payload = "{" . gcmsend_fillGeneralPayload($hash, $payload) . "}"; - - gcmsend_sendPayload($hash, $payload); - - return undef; + my @parts = split(/\|/, $message); + + my $tickerText; + my $contentTitle; + my $contentText; + my $notifyId = 1; + + my $length = int(@parts); + + if ($length == 3 || $length == 4) { + $tickerText = @parts[0]; + $contentTitle = @parts[1]; + $contentText = @parts[2]; + + if ($length == 4) { + my $notifyIdText = @parts[3]; + if (!(@parts[3] =~ m/[1-9][0-9]*/)) { + return "notifyId must be numeric and positive"; + } + $notifyId = @parts[3]; + } + } else { + return "Illegal message format. Required format is \r\n ". + "tickerText|contentTitle|contentText[|NotifyID]"; + } + my %payload = ( + "tickerText" => $tickerText, + "contentTitle" => $contentTitle, + "contentText" => $contentText, + "notifyId" => $notifyId, + "type" => "message" + ); + gcmsend_sendPayload($hash, %payload); + + return undef; } sub gcmsend_getLastDeviceStatesFor($$) { - my ($gcm, $deviceName) = @_; - - if (! $gcm->{STATES}) { - $gcm->{STATES} = {}; - } - - my $states = $gcm->{STATES}; - if (!$states->{$deviceName}) { - $states->{$deviceName} = {}; - } + my ($gcm, $deviceName) = @_; - return $states->{$deviceName}; + if (!$gcm->{STATES}) { + $gcm->{STATES} = { }; + } + + my $states = $gcm->{STATES}; + if (!$states->{$deviceName}) { + $states->{$deviceName} = { }; + } + + return $states->{$deviceName}; } sub gcmsend_notify($$) { - my ($gcm, $dev) = @_; - - my $logLevel = GetLogLevel($gcm,5); + my ($gcm, $dev) = @_; - my $name = $dev->{NAME}; - my $gcmName = $gcm->{NAME}; - - my $deviceFilter = AttrVal($gcm->{NAME}, "deviceFilter", ""); - - return if $name eq $gcmName; - return if(!$dev->{CHANGED}); # Some previous notify deleted the array. - return if (! ($deviceFilter eq "") && !($name =~ m/$deviceFilter/)); - - my $stateFilter = AttrVal($gcm->{NAME}, "stateFilter", ""); + my $logLevel = GetLogLevel($gcm, 5); - my $lastDeviceStates = gcmsend_getLastDeviceStatesFor($gcm, $name); + my $name = $dev->{NAME}; + my $gcmName = $gcm->{NAME}; - my $val = ""; - my $nrOfFieldChanges = int(@{$dev->{CHANGED}}); - my $sendFieldCount = 0; - - for (my $i = 0; $i < $nrOfFieldChanges; $i++) { - my @keyValue = split(":", $dev->{CHANGED}[$i]); - my $length = int($keyValue); + my $deviceFilter = AttrVal($gcm->{NAME}, "deviceFilter", ""); - my $change = $dev->{CHANGED}[$i]; + return if $name eq $gcmName; + return if (!$dev->{CHANGED}); # Some previous notify deleted the array. + return if (!($deviceFilter eq "") && !($name =~ m/$deviceFilter/)); + + my $stateFilter = AttrVal($gcm->{NAME}, "stateFilter", ""); + + my $lastDeviceStates = gcmsend_getLastDeviceStatesFor($gcm, $name); + + my $val = ""; + my $nrOfFieldChanges = int(@{$dev->{CHANGED}}); + my $sendFieldCount = 0; + + for (my $i = 0; $i < $nrOfFieldChanges; $i++) { + my @keyValue = split(":", $dev->{CHANGED}[$i]); + my $change = $dev->{CHANGED}[$i]; - # We need to find out a key and a value for each field update. - # For state updates, we have not field, which is why we simply - # put it to "state". - # For all other updates the notify value is delimited by ":", - # which we use to find out the value and the key. - my $key; - my $value; - my $position = index($change, ':'); - if ($position == -1) { - $key = "state"; - $value = $keyValue[0]; - } else { - $key = substr($change, 0, $position); - $value = substr($change, $position + 2, length($change)); + # We need to find out a key and a value for each field update. + # For state updates, we have not field, which is why we simply + # put it to "state". + # For all other updates the notify value is delimited by ":", + # which we use to find out the value and the key. + my $key; + my $value; + my $position = index($change, ':'); + if ($position == -1) { + $key = "state"; + $value = $keyValue[0]; + } else { + $key = substr($change, 0, $position); + $value = substr($change, $position + 2, length($change)); + } + + if (!($stateFilter eq "") && !($value =~ m/$stateFilter/)) { + Log $logLevel, + "$gcmName $name: ignoring $key, as value $value is blocked by stateFilter regexp."; + } elsif ($value eq "") { + Log $logLevel, "$gcmName $name: ignoring $key, as value is empty."; + } elsif ($lastDeviceStates->{$key} && $lastDeviceStates->{$key} eq $value) { + my $savedValue = $lastDeviceStates->{$key}; + Log $logLevel, + "$gcmName $name: ignoring $key, save value is $savedValue, value is $value"; + } else { + $lastDeviceStates->{$key} = $value; + # Multiple field updates are separated by <|>. + if ($sendFieldCount != 0) { + $val .= "<|>"; + } + $sendFieldCount += 1; + $val .= "$key:$value"; + } } - - if (! ($stateFilter eq "") && ! ($value =~ m/$stateFilter/)) { - Log $logLevel, "$gcmName $name: ignoring $key, as value $value is blocked by stateFilter regexp."; - } elsif ($value eq "") { - Log $logLevel, "$gcmName $name: ignoring $key, as value is empty."; - } elsif ($lastDeviceStates->{$key} && $lastDeviceStates->{$key} eq $value) { - my $savedValue = $lastDeviceStates->{$key}; - Log $logLevel, "$gcmName $name: ignoring $key, save value is $savedValue, value is $value"; - } else { - $lastDeviceStates->{$key} = $value; - # Multiple field updates are separated by <|>. - if ($sendFieldCount != 0) { - $val .= "<|>"; - } - $sendFieldCount += 1; - $val .= "$key:$value"; + if ($sendFieldCount > 0) { + gcmsend_sendNotify($gcm, $name, $val); } - } - if ($sendFieldCount > 0) { - gcmsend_sendNotify($gcm, $name, $val); - } -} +} 1; @@ -314,7 +364,8 @@ sub gcmsend_notify($$) Notes:
  • Module to send messages to GCM (Google Cloud Messaging).
  • -
  • Prerequisite is a GCM AcsendFieldCount with Google (see Google API Console
  • +
  • Prerequisite is a GCM Account (see Google API Console
  • +
  • Futhermore Crypt::CBC and Crypt::Cipher::AES Perl modules have to be installed
@@ -349,8 +400,7 @@ sub gcmsend_notify($$)
Make the receiving device vibrate upon receiving the message. Must be true or false.
  • attr <name> deviceFilter <regexp>
    Send a GCM notify only is the device name matches the given filter regexp.
  • -
  • -
    Specifies that the implementation of GCM should play a sound when an event is received. Note that andFHEM does not implement this attribute yet.
  • +
  • attr <name> cryptKey <key>
    Some key to encrypt message content. The key must have a size of 16 bytes. If the key length does not match it will be either cut or padded to the required length. As encryption algorithm AES is used.