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; sub gcmsend_Initialize($) { 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_attr { my ($cmd, $name, $aName, $aVal) = @_; if (not $aName eq "cryptKey") { return undef; } $aVal = sprintf("%016s", $aVal); $aVal = substr $aVal, length($aVal) - 16, 16; $_[3] = $aVal; return undef; } 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 @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(@args) != 2); $hash->{STATE} = 'Initialized'; return undef; } sub gcmsend_array_to_json(@) { my (@array) = @_; my $ret = ""; for (my $i = 0; $i < int(@array); $i++) { if ($i != 0) { $ret .= ","; } my $value = @array[$i]; $ret .= ("\"".$value."\""); } return "[".$ret."]"; } sub gcmsend_sendPayload($%) { my ($hash, %payload) = @_; my %generalPayload = gcmsend_getGeneralPayload($hash); my %toSendPayload = (%generalPayload, %payload); my %encryptedPayload = gcmsend_encrypt($hash, %toSendPayload); my $jsonPayload = gcmsend_toJson(%encryptedPayload); 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 $data = "{". "\"registration_ids\":".gcmsend_array_to_json(@registrationIds).",". "\"priority\": \"high\"" . "," . "\"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_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" ); 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, "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} = { }; } return $states->{$deviceName}; } sub gcmsend_notify($$) { my ($gcm, $dev) = @_; my $logLevel = GetLogLevel($gcm, 5); 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 $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)); } 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); } } 1; =pod =begin html

GCMSend

=end html =cut