98_gcmsend.pm 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412
  1. package main;
  2. use strict;
  3. use warnings;
  4. use HTTP::Request;
  5. use LWP::UserAgent;
  6. use IO::Socket::SSL;
  7. use utf8;
  8. use Crypt::CBC;
  9. use Crypt::Cipher::AES;
  10. sub gcmsend_Initialize($)
  11. {
  12. my ($hash) = @_;
  13. $hash->{DefFn} = "gcmsend_Define";
  14. $hash->{NotifyFn} = "gcmsend_notify";
  15. $hash->{AttrFn} = "gcmsend_attr";
  16. $hash->{SetFn} = "gcmsend_set";
  17. $hash->{AttrList} = "loglevel:0,1,2,3,4,5 regIds apiKey stateFilter vibrate deviceFilter cryptKey";
  18. }
  19. sub gcmsend_attr {
  20. my ($cmd, $name, $aName, $aVal) = @_;
  21. if (not $aName eq "cryptKey") {
  22. return undef;
  23. }
  24. $aVal = sprintf("%016s", $aVal);
  25. $aVal = substr $aVal, length($aVal) - 16, 16;
  26. $_[3] = $aVal;
  27. return undef;
  28. }
  29. sub gcmsend_set {
  30. my ($hash, @a) = @_;
  31. my $v = @a[1];
  32. if ($v eq "delete_saved_states") {
  33. $hash->{STATES} = { };
  34. return "deleted";
  35. } elsif ($v eq "send") {
  36. my $msg = "";
  37. for (my $i = 2; $i < int(@a); $i++) {
  38. if (!($msg eq "")) {
  39. $msg .= " ";
  40. }
  41. $msg .= @a[$i];
  42. }
  43. return gcmsend_sendMessage($hash, $msg);
  44. } else {
  45. return "unknown set value, choose one of delete_saved_states send";
  46. }
  47. }
  48. sub gcmsend_Define($$)
  49. {
  50. my ($hash, $def) = @_;
  51. my @args = split("[ \t]+", $def);
  52. if (int(@args) < 1)
  53. {
  54. return "gcmsend_Define: too many arguments. Usage:\n".
  55. "define <name> gcmsend";
  56. }
  57. return "Invalid arguments. Usage: \n define <name> gcmsend" if (int(@args) != 2);
  58. $hash->{STATE} = 'Initialized';
  59. return undef;
  60. }
  61. sub gcmsend_array_to_json(@) {
  62. my (@array) = @_;
  63. my $ret = "";
  64. for (my $i = 0; $i < int(@array); $i++) {
  65. if ($i != 0) {
  66. $ret .= ",";
  67. }
  68. my $value = @array[$i];
  69. $ret .= ("\"".$value."\"");
  70. }
  71. return "[".$ret."]";
  72. }
  73. sub gcmsend_sendPayload($%) {
  74. my ($hash, %payload) = @_;
  75. my %generalPayload = gcmsend_getGeneralPayload($hash);
  76. my %toSendPayload = (%generalPayload, %payload);
  77. my %encryptedPayload = gcmsend_encrypt($hash, %toSendPayload);
  78. my $jsonPayload = gcmsend_toJson(%encryptedPayload);
  79. my $name = $hash->{NAME};
  80. my $logLevel = GetLogLevel($name, 5);
  81. my $client = LWP::UserAgent->new();
  82. my $regIdsText = AttrVal($name, "regIds", "");
  83. my $apikey = AttrVal($name, "apiKey", "");
  84. my @registrationIds = split(/\|/, $regIdsText);
  85. if (int(@registrationIds) == 0) {
  86. Log $logLevel, "$name no registrationIds set.";
  87. return undef;
  88. }
  89. return undef if (int(@registrationIds) == 0);
  90. my $data =
  91. "{".
  92. "\"registration_ids\":".gcmsend_array_to_json(@registrationIds).",".
  93. "\"priority\": \"high\"" . "," .
  94. "\"data\": $jsonPayload".
  95. "}";
  96. Log $logLevel, "data is $jsonPayload";
  97. my $req = HTTP::Request->new( POST => "https://android.googleapis.com/gcm/send" );
  98. $req->header( Authorization => 'key='.$apikey );
  99. $req->header( 'Content-Type' => 'application/json; charset=UTF-8' );
  100. $req->content( $data );
  101. my $response = $client->request( $req );
  102. if (!$response->is_success) {
  103. Log $logLevel, "error during request: ".$response->status_line;
  104. $hash->{STATE} = $response->status_line;
  105. }
  106. $hash->{STATE} = "OK";
  107. return undef;
  108. }
  109. sub gcmsend_getGeneralPayload($) {
  110. my ($hash) = @_;
  111. my $name = $hash->{NAME};
  112. my $vibrate = "false";
  113. if (AttrVal($name, "vibrate", "false") eq "true") {
  114. $vibrate = "true";
  115. }
  116. my $gcmName = $hash->{NAME};
  117. my %generalPayload = (
  118. "source" => "gcmsend_fhem",
  119. "gcmDeviceName" => $gcmName,
  120. "vibrate" => "$vibrate"
  121. );
  122. return %generalPayload;
  123. }
  124. sub gcmsend_sendNotify($$$) {
  125. my ($hash, $deviceName, $changes) = @_;
  126. my %payload = (
  127. "deviceName" => $deviceName,
  128. "changes" => $changes,
  129. "type" => "notify"
  130. );
  131. gcmsend_sendPayload($hash, %payload);
  132. }
  133. sub gcmsend_toJson(%) {
  134. my (%hash) = @_;
  135. my @entries = ();
  136. while (my ($key, $value) = each %hash) {
  137. my $entry = "\"$key\":\"$value\"";
  138. push @entries, $entry;
  139. }
  140. return "{".join(", ", @entries)."}";
  141. }
  142. my %gcmsend_encrypt_keys = ("type" => "", "notifyId" => "", "changes" => "", "deviceName" => "",
  143. "tickerText" => "", "contentText" => "", "contentTitle" => "");
  144. sub gcmsend_encrypt($%) {
  145. my ($hash, %payload) = @_;
  146. my $key = AttrVal($hash->{NAME}, "cryptKey", "");
  147. if ($key eq "") {
  148. return %payload;
  149. }
  150. my $cipher = Crypt::CBC->new(
  151. -cipher => 'Crypt::Cipher::AES',
  152. -key => $key,
  153. -iv => $key,
  154. -padding => 'standard',
  155. -header => 'none',
  156. -blocksize => '16',
  157. -literal_key => 1,
  158. -keysize => 16
  159. );
  160. my %newPayload = ();
  161. while (my ($key, $value) = each %payload) {
  162. if (exists(%gcmsend_encrypt_keys->{$key})) {
  163. my $padded = sprintf '%16s', $value;
  164. my $length = length($padded);
  165. %newPayload->{$key} = $cipher->encrypt_hex( $value );
  166. } else {
  167. %newPayload->{$key} = $value;
  168. }
  169. }
  170. return %newPayload;
  171. }
  172. sub gcmsend_sendMessage($$) {
  173. my ($hash, $message) = @_;
  174. my @parts = split(/\|/, $message);
  175. my $tickerText;
  176. my $contentTitle;
  177. my $contentText;
  178. my $notifyId = 1;
  179. my $length = int(@parts);
  180. if ($length == 3 || $length == 4) {
  181. $tickerText = @parts[0];
  182. $contentTitle = @parts[1];
  183. $contentText = @parts[2];
  184. if ($length == 4) {
  185. my $notifyIdText = @parts[3];
  186. if (!(@parts[3] =~ m/[1-9][0-9]*/)) {
  187. return "notifyId must be numeric and positive";
  188. }
  189. $notifyId = @parts[3];
  190. }
  191. } else {
  192. return "Illegal message format. Required format is \r\n ".
  193. "tickerText|contentTitle|contentText[|NotifyID]";
  194. }
  195. my %payload = (
  196. "tickerText" => $tickerText,
  197. "contentTitle" => $contentTitle,
  198. "contentText" => $contentText,
  199. "notifyId" => $notifyId,
  200. "type" => "message"
  201. );
  202. gcmsend_sendPayload($hash, %payload);
  203. return undef;
  204. }
  205. sub gcmsend_getLastDeviceStatesFor($$)
  206. {
  207. my ($gcm, $deviceName) = @_;
  208. if (!$gcm->{STATES}) {
  209. $gcm->{STATES} = { };
  210. }
  211. my $states = $gcm->{STATES};
  212. if (!$states->{$deviceName}) {
  213. $states->{$deviceName} = { };
  214. }
  215. return $states->{$deviceName};
  216. }
  217. sub gcmsend_notify($$)
  218. {
  219. my ($gcm, $dev) = @_;
  220. my $logLevel = GetLogLevel($gcm, 5);
  221. my $name = $dev->{NAME};
  222. my $gcmName = $gcm->{NAME};
  223. my $deviceFilter = AttrVal($gcm->{NAME}, "deviceFilter", "");
  224. return if $name eq $gcmName;
  225. return if (!$dev->{CHANGED}); # Some previous notify deleted the array.
  226. return if (!($deviceFilter eq "") && !($name =~ m/$deviceFilter/));
  227. my $stateFilter = AttrVal($gcm->{NAME}, "stateFilter", "");
  228. my $lastDeviceStates = gcmsend_getLastDeviceStatesFor($gcm, $name);
  229. my $val = "";
  230. my $nrOfFieldChanges = int(@{$dev->{CHANGED}});
  231. my $sendFieldCount = 0;
  232. for (my $i = 0; $i < $nrOfFieldChanges; $i++) {
  233. my @keyValue = split(":", $dev->{CHANGED}[$i]);
  234. my $change = $dev->{CHANGED}[$i];
  235. # We need to find out a key and a value for each field update.
  236. # For state updates, we have not field, which is why we simply
  237. # put it to "state".
  238. # For all other updates the notify value is delimited by ":",
  239. # which we use to find out the value and the key.
  240. my $key;
  241. my $value;
  242. my $position = index($change, ':');
  243. if ($position == -1) {
  244. $key = "state";
  245. $value = $keyValue[0];
  246. } else {
  247. $key = substr($change, 0, $position);
  248. $value = substr($change, $position + 2, length($change));
  249. }
  250. if (!($stateFilter eq "") && !($value =~ m/$stateFilter/)) {
  251. Log $logLevel,
  252. "$gcmName $name: ignoring $key, as value $value is blocked by stateFilter regexp.";
  253. } elsif ($value eq "") {
  254. Log $logLevel, "$gcmName $name: ignoring $key, as value is empty.";
  255. } elsif ($lastDeviceStates->{$key} && $lastDeviceStates->{$key} eq $value) {
  256. my $savedValue = $lastDeviceStates->{$key};
  257. Log $logLevel,
  258. "$gcmName $name: ignoring $key, save value is $savedValue, value is $value";
  259. } else {
  260. $lastDeviceStates->{$key} = $value;
  261. # Multiple field updates are separated by <|>.
  262. if ($sendFieldCount != 0) {
  263. $val .= "<|>";
  264. }
  265. $sendFieldCount += 1;
  266. $val .= "$key:$value";
  267. }
  268. }
  269. if ($sendFieldCount > 0) {
  270. gcmsend_sendNotify($gcm, $name, $val);
  271. }
  272. }
  273. 1;
  274. =pod
  275. =begin html
  276. <a name="GCMSend"></a>
  277. <h3>GCMSend</h3>
  278. <ul>
  279. Google Cloud Messaging (GCM) is a toolset to send push notifications to Android handset
  280. devices. This can be used to refresh the internal state of, for example, andFHEM to achieve
  281. a nearly up-to-date internal state of other applications. <br/>
  282. The module pushes any internal updates to GCM, which can be used by other apps. As payload,
  283. there is a data hash including the deviceName, the source (which is always gcmsend_fhem) and
  284. an amount of changes. The changes are concatenated by "<|>", whereas each change itself is formatted
  285. like "key:value". <br />
  286. For instance, the changes could look like: "state:on<|>measured:2013-08-11".
  287. <br />
  288. Note: If not receiving messages, make sure to increase the log level of this device. Afterwards,
  289. have a look at the log messages - the module is quite verbose.
  290. <br><br>
  291. <a name="GCMSenddefine"></a>
  292. <h4>Define</h4>
  293. <ul>
  294. <code>define &lt;name&gt; gcmsend</code>
  295. <br><br>
  296. Defines a GCMSend device.<br><br>
  297. Example:
  298. <ul>
  299. <code>define gcm gcmsend</code><br>
  300. </ul>
  301. Notes:
  302. <ul>
  303. <li>Module to send messages to GCM (Google Cloud Messaging).</li>
  304. <li>Prerequisite is a GCM Account (see <a href="https://code.google.com/apis/console/">Google API Console</a></li>
  305. <li>Futhermore <code>Crypt::CBC</code> and <code>Crypt::Cipher::AES</code> Perl modules have to be installed
  306. </ul>
  307. </ul>
  308. <a name="GCMSendSet"></a>
  309. <h4>Set </h4>
  310. <ul>
  311. <code>set &lt;name&gt; &lt;value&gt;</code>
  312. <br><br>
  313. where <code>value</code> is one of:<br>
  314. <pre>
  315. delete_saved_states # deletes all saved states
  316. send # send a message (tickerText|contentTitle|contentText[|NotifyID])
  317. </pre>
  318. Examples:
  319. <ul>
  320. <code>set gcm delete_saved_states</code><br>
  321. <code>set gcm send ticker text|my title|my text|5</code><br/>
  322. </ul>
  323. </ul>
  324. <a name="GCMSendAttr"></a>
  325. <h4>Attributes</h4>
  326. <ul>
  327. <li><a name="gcmsend_regIds"><code>attr &lt;name&gt; regIds &lt;string&gt;</code></a>
  328. <br />Registration IDs Google sends the messages to (multiple values separated by "|"</li>
  329. <li><a name="gcmsend_apiKey"><code>attr &lt;name&gt; apiKey &lt;string&gt;</code></a>
  330. <br />API-Key for GCM (can be found within the Google API Console)</li>
  331. <li><a name="gcmsend_stateFilter"><code>attr &lt;name&gt; stateFilter &lt;regexp&gt;</code></a>
  332. <br />Send a GCM message only if the attribute matches the attribute filter regexp</li>
  333. <li><a name="gcmsend_vibrate"><code>attr &lt;name&gt; vibrate (true|false)</a>
  334. <br />Make the receiving device vibrate upon receiving the message. Must be true or false.</li>
  335. <li><a name="gcmsend_deviceFilter"><code>attr &lt;name&gt; deviceFilter &lt;regexp&gt;</a>
  336. <br />Send a GCM notify only is the device name matches the given filter regexp.</li>
  337. <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>
  338. </ul>
  339. </ul>
  340. =end html
  341. =cut