| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412 |
- 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 <name> gcmsend";
- }
- return "Invalid arguments. Usage: \n define <name> 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
- <a name="GCMSend"></a>
- <h3>GCMSend</h3>
- <ul>
- Google Cloud Messaging (GCM) is a toolset to send push notifications to Android handset
- devices. This can be used to refresh the internal state of, for example, andFHEM to achieve
- a nearly up-to-date internal state of other applications. <br/>
- The module pushes any internal updates to GCM, which can be used by other apps. As payload,
- there is a data hash including the deviceName, the source (which is always gcmsend_fhem) and
- an amount of changes. The changes are concatenated by "<|>", whereas each change itself is formatted
- like "key:value". <br />
- For instance, the changes could look like: "state:on<|>measured:2013-08-11".
- <br />
- Note: If not receiving messages, make sure to increase the log level of this device. Afterwards,
- have a look at the log messages - the module is quite verbose.
- <br><br>
- <a name="GCMSenddefine"></a>
- <h4>Define</h4>
- <ul>
- <code>define <name> gcmsend</code>
- <br><br>
- Defines a GCMSend device.<br><br>
- Example:
- <ul>
- <code>define gcm gcmsend</code><br>
- </ul>
- Notes:
- <ul>
- <li>Module to send messages to GCM (Google Cloud Messaging).</li>
- <li>Prerequisite is a GCM Account (see <a href="https://code.google.com/apis/console/">Google API Console</a></li>
- <li>Futhermore <code>Crypt::CBC</code> and <code>Crypt::Cipher::AES</code> Perl modules have to be installed
- </ul>
- </ul>
- <a name="GCMSendSet"></a>
- <h4>Set </h4>
- <ul>
- <code>set <name> <value></code>
- <br><br>
- where <code>value</code> is one of:<br>
- <pre>
- delete_saved_states # deletes all saved states
- send # send a message (tickerText|contentTitle|contentText[|NotifyID])
- </pre>
- Examples:
- <ul>
- <code>set gcm delete_saved_states</code><br>
- <code>set gcm send ticker text|my title|my text|5</code><br/>
- </ul>
- </ul>
- <a name="GCMSendAttr"></a>
- <h4>Attributes</h4>
- <ul>
- <li><a name="gcmsend_regIds"><code>attr <name> regIds <string></code></a>
- <br />Registration IDs Google sends the messages to (multiple values separated by "|"</li>
- <li><a name="gcmsend_apiKey"><code>attr <name> apiKey <string></code></a>
- <br />API-Key for GCM (can be found within the Google API Console)</li>
- <li><a name="gcmsend_stateFilter"><code>attr <name> stateFilter <regexp></code></a>
- <br />Send a GCM message only if the attribute matches the attribute filter regexp</li>
- <li><a name="gcmsend_vibrate"><code>attr <name> vibrate (true|false)</a>
- <br />Make the receiving device vibrate upon receiving the message. Must be true or false.</li>
- <li><a name="gcmsend_deviceFilter"><code>attr <name> deviceFilter <regexp></a>
- <br />Send a GCM notify only is the device name matches the given filter regexp.</li>
- <li><a name="gcmsend_cryptKey"><code>attr <name> cryptKey <key></a> <br/>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.</li>
- </ul>
- </ul>
- =end html
- =cut
|