2
0
mirror of https://github.com/fhem/fhem-mirror.git synced 2024-11-22 02:59:49 +00:00
fhem-mirror/fhem/contrib/98_gcmsend.pm

412 lines
12 KiB
Perl

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 &lt;name&gt; 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 &lt;name&gt; &lt;value&gt;</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 &lt;name&gt; regIds &lt;string&gt;</code></a>
<br />Registration IDs Google sends the messages to (multiple values separated by "|"</li>
<li><a name="gcmsend_apiKey"><code>attr &lt;name&gt; apiKey &lt;string&gt;</code></a>
<br />API-Key for GCM (can be found within the Google API Console)</li>
<li><a name="gcmsend_stateFilter"><code>attr &lt;name&gt; stateFilter &lt;regexp&gt;</code></a>
<br />Send a GCM message only if the attribute matches the attribute filter regexp</li>
<li><a name="gcmsend_vibrate"><code>attr &lt;name&gt; 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 &lt;name&gt; deviceFilter &lt;regexp&gt;</a>
<br />Send a GCM notify only is the device name matches the given filter regexp.</li>
<li><a name="gcmsend_cryptKey"><code>attr &lt;name&gt; cryptKey &lt;key&gt;</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