74_UnifiVideo.pm 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670
  1. # $Id: 74_UnifiVideo.pm 16158 2018-02-12 15:39:47Z justme1968 $
  2. package main;
  3. use strict;
  4. use warnings;
  5. use JSON;
  6. use Data::Dumper;
  7. use HttpUtils;
  8. use vars qw(%modules);
  9. use vars qw(%defs);
  10. use vars qw(%attr);
  11. use vars qw($readingFnAttributes);
  12. sub Log($$);
  13. sub Log3($$$);
  14. sub
  15. UnifiVideo_Initialize($)
  16. {
  17. my ($hash) = @_;
  18. $hash->{ReadFn} = "UnifiVideo_Read";
  19. $hash->{DefFn} = "UnifiVideo_Define";
  20. $hash->{NotifyFn} = "UnifiVideo_Notify";
  21. $hash->{UndefFn} = "UnifiVideo_Undefine";
  22. $hash->{SetFn} = "UnifiVideo_Set";
  23. $hash->{GetFn} = "UnifiVideo_Get";
  24. $hash->{AttrFn} = "UnifiVideo_Attr";
  25. $hash->{AttrList} = "disable filePath apiKey ".
  26. "sshUser ".
  27. $readingFnAttributes;
  28. $hash->{FW_detailFn} = "UnifiVideo_detailFn";
  29. }
  30. #####################################
  31. sub
  32. UnifiVideo_Define($$)
  33. {
  34. my ($hash, $def) = @_;
  35. my @a = split("[ \t][ \t]*", $def);
  36. return "Usage: define <name> UnifiVideo <ip> [<apiKey>]" if(@a < 3);
  37. my $name = $a[0];
  38. my $host = $a[2];
  39. $hash->{NAME} = $name;
  40. my $d = $modules{$hash->{TYPE}}{defptr};
  41. return "$hash->{TYPE} device already defined as $d->{NAME}." if( defined($d) && $name ne $d->{NAME} );
  42. $modules{$hash->{TYPE}}{defptr} = $hash;
  43. $hash->{NOTIFYDEV} = "global";
  44. $hash->{HOST} = $host;
  45. $hash->{DEF} = $host;
  46. $hash->{STATE} = 'active';
  47. CommandAttr(undef,"$name apiKey $a[3]" ) if( defined($a[3]) );
  48. if( $init_done ) {
  49. UnifiVideo_Connect($hash);
  50. } else {
  51. readingsSingleUpdate($hash, 'state', 'initialized', 1 );
  52. }
  53. return undef;
  54. }
  55. sub
  56. UnifiVideo_Notify($$)
  57. {
  58. my ($hash,$dev) = @_;
  59. return if($dev->{NAME} ne "global");
  60. return if(!grep(m/^INITIALIZED|REREADCFG$/, @{$dev->{CHANGED}}));
  61. UnifiVideo_Connect($hash);
  62. return undef;
  63. }
  64. sub
  65. UnifiVideo_Undefine($$)
  66. {
  67. my ($hash, $arg) = @_;
  68. UnifiVideo_killLogWatcher($hash);
  69. RemoveInternalTimer($hash, "UnifiVideo_Connect");
  70. delete $modules{$hash->{TYPE}}{defptr};
  71. return undef;
  72. }
  73. sub
  74. UnifiVideo_detailFn()
  75. {
  76. my ($FW_wname, $d, $room, $extPage) = @_; # extPage is set for summaryFn.
  77. my $hash = $defs{$d};
  78. return UnifiVideo_2html($hash);
  79. }
  80. sub
  81. UnifiVideo_2html($;$$)
  82. {
  83. my ($hash,$cams,$width) = @_;
  84. $hash = $defs{$hash} if( ref($hash) ne 'HASH' );
  85. return undef if( !defined($hash) );
  86. $width = 200 if( !$width );
  87. my $name = $hash->{NAME};
  88. my @cams = split(',', $cams) if( defined($cams) );
  89. my $apiKey = AttrVal($name, 'apiKey', undef);
  90. return undef if( !$apiKey );
  91. $apiKey = UnifiVideo_decrypt( $apiKey );
  92. my $json = $hash->{helper}{json};
  93. return undef if( !$json );
  94. my $javascriptText = "<script type='text/javascript'>
  95. function loadImages() {
  96. var tags = document.getElementsByClassName('unifiSnap');
  97. for(var i = 0;i < tags.length; i++) {
  98. var img = tags[i];
  99. var nvrIp = img.getAttribute('nvrIp');
  100. var cameraId = img.getAttribute('cameraId');
  101. var apiKey = img.getAttribute('apiKey');
  102. var width = img.width;
  103. tags[i].src='http://'+ nvrIp +':7080/api/2.0/snapshot/camera/'+cameraId+'?force=true&width='+width+'&apiKey='+apiKey+'&'+Date.now();
  104. }
  105. setTimeout( function() {loadImages()}, 1000 );
  106. }
  107. </script>";
  108. $javascriptText =~ s/\n/ /g;
  109. $javascriptText =~ s/ +/ /g;
  110. my $html = "$javascriptText<div onload='loadImages()'>";
  111. $html .= "\n" if( $html );
  112. $html .= '<iframe style="display:none" onload="loadImages()"></iframe>';
  113. my $i = 0;
  114. foreach my $entry (@{$json->{data}}) {
  115. next if( $entry->{deleted} );
  116. next if( $entry->{state} eq 'DISCONNECTED' );
  117. if( defined($cams) ) {
  118. foreach my $cam (@cams) {
  119. if( ( $cam =~ m/[0-9]+/ && int($cam) == $i )
  120. || $entry->{_id} eq $cam
  121. || $entry->{name} =~ m/$cam/ ) {
  122. $html .= "\n" if( $html );
  123. $html .= " <img width='$width' class='unifiSnap' nvrIp='$hash->{HOST}' apiKey='$apiKey' cameraId='$entry->{_id}'>";
  124. }
  125. }
  126. } else {
  127. $html .= "\n" if( $html );
  128. $html .= " <img width='200' class='unifiSnap' nvrIp='$hash->{HOST}' apiKey='$apiKey' cameraId='$entry->{_id}'>";
  129. }
  130. ++$i;
  131. }
  132. $html .= "\n" if( $html );
  133. $html .= "</div>";
  134. #Log 1, $html;
  135. return $html;
  136. }
  137. sub
  138. UnifiVideo_Set($$@)
  139. {
  140. my ($hash, $name, $cmd, @args) = @_;
  141. my $list = "reconnect:noArg snapshot apiKey";
  142. if( $cmd eq 'reconnect' ) {
  143. $hash->{".triggerUsed"} = 1;
  144. UnifiVideo_Connect($hash);
  145. return undef;
  146. } elsif( $cmd eq 'snapshot' ) {
  147. my $json = $hash->{helper}{json};
  148. return "not jet connected" if( !$json );
  149. my ($param_a, $param_h) = parseParams(\@args);
  150. my $cam = $param_h->{cam};
  151. my $width = $param_h->{width};
  152. return "usage: snapshot cam=<cam> [width=<width>] [fileName=<fileName>]" if( !defined($cam) );
  153. my $i = 0;
  154. foreach my $entry (@{$json->{data}}) {
  155. next if( $entry->{deleted} );
  156. next if( $entry->{state} eq 'DISCONNECTED' );
  157. if( ( $cam =~ m/[0-9]+/ && int($cam) == $i )
  158. || $entry->{_id} eq $cam
  159. || $entry->{name} =~ m/$cam/ ) {
  160. $cam = $entry->{_id};
  161. #Log 1, "$i $entry->{name}: $entry->{_id}";
  162. last;
  163. }
  164. ++$i;
  165. }
  166. return "no such cam: $cam" if( $i >= $json->{meta}{totalCount} );
  167. my $apiKey = AttrVal($name, 'apiKey', undef);
  168. $apiKey = UnifiVideo_decrypt( $apiKey );
  169. my $url = "http://$hash->{HOST}:7080/api/2.0/snapshot/camera/$cam?force=true&apiKey=$apiKey";
  170. $url .= "&width=$width" if( $width );
  171. my $param = {
  172. url => $url,
  173. method => 'GET',
  174. timeout => 5,
  175. noshutdown => 0,
  176. hash => $hash,
  177. key => 'snapshot',
  178. cam => $cam,
  179. fileName => $param_h->{fileName} ,
  180. index => $i,
  181. };
  182. Log3 $name, 4, "$name: fetching data from $url";
  183. $param->{callback} = \&UnifiVideo_parseHttpAnswer;
  184. HttpUtils_NonblockingGet( $param );
  185. return undef;
  186. } elsif( $cmd eq 'apiKey' ) {
  187. return CommandAttr(undef,"$name apiKey $args[0]" );
  188. }
  189. return "Unknown argument $cmd, choose one of $list";
  190. }
  191. sub
  192. UnifiVideo_Get($$@)
  193. {
  194. my ($hash, $name, $cmd) = @_;
  195. my $list = "apiKey:noArg";
  196. if( $cmd eq 'apiKey' ) {
  197. my $apiKey = AttrVal($name, 'apiKey', undef);
  198. return 'no apiKey set' if( !$apiKey );
  199. $apiKey = UnifiVideo_decrypt( $apiKey );
  200. return "apiKey: $apiKey";
  201. }
  202. return "Unknown argument $cmd, choose one of $list";
  203. }
  204. sub
  205. UnifiVideo_Parse($$;$)
  206. {
  207. my ($hash,$data,$peerhost) = @_;
  208. my $name = $hash->{NAME};
  209. }
  210. sub
  211. UnifiVideo_parseHttpAnswer($$$)
  212. {
  213. my ($param, $err, $data) = @_;
  214. my $hash = $param->{hash};
  215. my $name = $hash->{NAME};
  216. if( $err ) {
  217. Log3 $name, 2, "$name: http request ($param->{url}) failed: $err";
  218. return undef;
  219. }
  220. return undef if( !$data );
  221. Log3 $name, 5, "$name: received $data";
  222. if( $param->{key} eq 'json' ) {
  223. my $json = eval { decode_json($data) };
  224. Log3 $name, 2, "$name: json error: $@ in $json" if( $@ );
  225. #Log 1, Dumper $json;
  226. $hash->{helper}{json} = $json;
  227. if( !defined( $json->{meta} ) ) {
  228. Log3 $name, 2, "$name: received unknown data";
  229. return undef;
  230. }
  231. my $apiKey = AttrVal($name, 'apiKey', undef);
  232. $apiKey = UnifiVideo_decrypt( $apiKey );
  233. my $totalCount = $json->{meta}{totalCount};
  234. readingsBeginUpdate($hash);
  235. readingsBulkUpdate($hash, 'state', $hash->{PID}?'watching':'running', 1 );
  236. readingsBulkUpdateIfChanged($hash, 'totalCount', $totalCount, 1);
  237. my $i = 0;
  238. foreach my $entry (@{$json->{data}}) {
  239. if( !$entry->{deleted} ) {
  240. #Log 1, Dumper $entry->{_id};
  241. readingsBulkUpdateIfChanged($hash, "cam${i}name", $entry->{name}, 1);
  242. readingsBulkUpdateIfChanged($hash, "cam${i}id", $entry->{_id}, 1);
  243. readingsBulkUpdateIfChanged($hash, "cam${i}state", $entry->{state}, 1);
  244. #readingsBulkUpdateIfChanged($hash, "cam${i}snapshotURL", "http://$hash->{HOST}:7080/api/2.0/snapshot/camera/$entry->{_id}?force=true&apiKey=$apiKey" , 1);
  245. }
  246. ++$i;
  247. }
  248. readingsEndUpdate($hash,1);
  249. RemoveInternalTimer($hash, "UnifiVideo_Connect");
  250. InternalTimer(gettimeofday() + 900, "UnifiVideo_Connect", $hash);
  251. } elsif( $param->{key} eq 'snapshot' ) {
  252. my $modpath = $attr{global}{modpath};
  253. my $filePath = AttrVal($name, 'filePath', "$modpath/www/snapshots" );
  254. if(! -d $filePath) {
  255. my $ret = mkdir "$filePath";
  256. if($ret == 0) {
  257. Log3 $name, 1, "Error while creating filePath $filePath $!";
  258. return undef;
  259. }
  260. }
  261. my $fileName = $param->{fileName};
  262. $fileName = $param->{cam} if( !$fileName );
  263. $fileName .= '.jpg';
  264. if(!open(FH, ">$filePath/$fileName")) {
  265. Log3 $name, 1, "Can't write $filePath/$fileName $!";
  266. return undef;
  267. }
  268. print FH $data;
  269. close(FH);
  270. Log3 $name, 4, "snapshot $filePath/$fileName written.";
  271. DoTrigger( $name, "newSnapshot: $param->{index} $filePath/$fileName" );
  272. } else {
  273. Log3 $name, 2, "parseHttpAnswer: unhandled key";
  274. }
  275. return undef;
  276. }
  277. sub
  278. UnifiVideo_Read($)
  279. {
  280. my ($hash) = @_;
  281. my $name = $hash->{NAME};
  282. my $buf;
  283. my $ret = sysread($hash->{FH}, $buf, 65536 );
  284. my $err = int($!);
  285. if(!defined($ret) && $err == EWOULDBLOCK) {
  286. return;
  287. }
  288. #Log 1, $ret;
  289. #Log 1, $buf;
  290. #Log 1, $err;
  291. if( $ret == 0 && !defined($hash->{PARTIAL}) ) {
  292. UnifiVideo_killLogWatcher($hash);
  293. }
  294. my $data = $hash->{PARTIAL};
  295. $data .= $buf;
  296. while($data =~ m/\n/) {
  297. my $line;
  298. ($line,$data) = split("\n", $data, 2);
  299. if( $line =~ m/password/ ) {
  300. UnifiVideo_killLogWatcher($hash);
  301. } elsif( $line =~ m/Camera\[([^\]]+)\].*type:([^\s]+)/ ) {
  302. my $cam = $1;
  303. my $type = $2;
  304. if( $type eq 'start' ) {
  305. my $json = $hash->{helper}{json};
  306. $json = [] if( !$json );
  307. my $i = 0;
  308. foreach my $entry (@{$json->{data}}) {
  309. last if( $entry->{mac} eq $cam );
  310. ++$i;
  311. }
  312. if( $i >= $json->{meta}{totalCount} ) {
  313. Log3 $name, 2, "$name: got motion event for unknown cam: $cam";
  314. } else {
  315. readingsSingleUpdate($hash, "cam${i}motion", $type, 1);
  316. }
  317. } elsif( $type eq 'stop' ) {
  318. } else {
  319. Log3 $name, 2, "$name: got unknown event type from cam: $cam";
  320. }
  321. } else {
  322. Log3 $name, 2, "$name: got unknown event: $line";
  323. }
  324. }
  325. $hash->{PARTIAL} = $data
  326. #UnifiVideo_Parse($hash, $buf, $hash->{CD}->peerhost);
  327. }
  328. sub
  329. UnifiVideo_killLogWatcher($)
  330. {
  331. my ($hash) = @_;
  332. my $name = $hash->{NAME};
  333. kill( 9, $hash->{PID} ) if( $hash->{PID} );
  334. close($hash->{FH}) if($hash->{FH});
  335. delete($hash->{FH});
  336. delete($hash->{FD});
  337. return if( !$hash->{PID} );
  338. delete $hash->{PID};
  339. readingsSingleUpdate($hash, 'state', 'running', 1 );
  340. Log3 $name, 3, "$name: stopped logfile watcher";
  341. delete $hash->{PARTIAL};
  342. delete($selectlist{$name});
  343. Log 1, "4";
  344. }
  345. sub
  346. UnifiVideo_startLogWatcher($)
  347. {
  348. my ($hash) = @_;
  349. my $name = $hash->{NAME};
  350. UnifiVideo_killLogWatcher($hash);
  351. my $user = AttrVal($name, "sshUser", undef);
  352. return if( !$user );
  353. my $logfile = AttrVal($name, "logfile", "/var/log/unifi-video/motion.log" );
  354. my $cmd = qx(which ssh);
  355. chomp( $cmd );
  356. $cmd .= ' -q ';
  357. $cmd .= $user."\@" if( defined($user) );
  358. $cmd .= $hash->{HOST};
  359. $cmd .= " tail -n 0 -F $logfile";
  360. #my $cmd = "tail -f /tmp/x";
  361. Log3 $name, 3, "$name: using $cmd to watch logfile";
  362. if( my $pid = open( my $fh, '-|', $cmd ) ) {
  363. $fh->blocking(0);
  364. $hash->{FH} = $fh;
  365. $hash->{FD} = fileno($fh);
  366. $hash->{PID} = $pid;
  367. $selectlist{$name} = $hash;
  368. readingsSingleUpdate($hash, 'state', 'watching', 1 );
  369. Log3 $name, 3, "$name: started logfile watcher";
  370. } else {
  371. Log3 $name, 2, "$name: failed to start logfile watcher";
  372. }
  373. }
  374. sub
  375. UnifiVideo_Connect($)
  376. {
  377. my ($hash) = @_;
  378. my $name = $hash->{NAME};
  379. return if( IsDisabled($name) );
  380. my $apiKey = AttrVal($name, 'apiKey', undef);
  381. if( !$apiKey ) {
  382. $hash->{STATE} = 'disconnected';
  383. Log3 $name, 2, "$name: can't connect without apiKey";
  384. return undef;
  385. }
  386. $apiKey = UnifiVideo_decrypt( $apiKey );
  387. my $url = "http://$hash->{HOST}:7080/api/2.0/camera?apiKey=$apiKey";
  388. my $param = {
  389. url => $url,
  390. method => 'GET',
  391. timeout => 5,
  392. noshutdown => 0,
  393. hash => $hash,
  394. key => 'json',
  395. };
  396. Log3 $name, 4, "$name: fetching data from $url";
  397. $param->{callback} = \&UnifiVideo_parseHttpAnswer;
  398. HttpUtils_NonblockingGet( $param );
  399. UnifiVideo_startLogWatcher( $hash ) if( !$hash->{PID} );
  400. return undef;
  401. }
  402. sub
  403. UnifiVideo_encrypt($)
  404. {
  405. my ($decoded) = @_;
  406. my $key = getUniqueId();
  407. my $encoded;
  408. return $decoded if( $decoded =~ m/^crypt:(.*)/ );
  409. for my $char (split //, $decoded) {
  410. my $encode = chop($key);
  411. $encoded .= sprintf("%.2x",ord($char)^ord($encode));
  412. $key = $encode.$key;
  413. }
  414. return 'crypt:'. $encoded;
  415. }
  416. sub
  417. UnifiVideo_decrypt($)
  418. {
  419. my ($encoded) = @_;
  420. my $key = getUniqueId();
  421. my $decoded;
  422. $encoded = $1 if( $encoded =~ m/^crypt:(.*)/ );
  423. for my $char (map { pack('C', hex($_)) } ($encoded =~ m/(..)/g)) {
  424. my $decode = chop($key);
  425. $decoded .= chr(ord($char)^ord($decode));
  426. $key = $decode.$key;
  427. }
  428. return $decoded;
  429. }
  430. sub
  431. UnifiVideo_Attr($$$)
  432. {
  433. my ($cmd, $name, $attrName, $attrVal) = @_;
  434. my $orig = $attrVal;
  435. my $hash = $defs{$name};
  436. if( $attrName eq 'disable' ) {
  437. if( $cmd eq "set" && $attrVal ) {
  438. UnifiVideo_killLogWatcher($hash);
  439. readingsSingleUpdate($hash, 'state', 'disabled', 1 );
  440. } else {
  441. readingsSingleUpdate($hash, 'state', 'running', 1 );
  442. $attr{$name}{$attrName} = 0;
  443. UnifiVideo_Connect($hash);
  444. }
  445. } elsif( $attrName eq 'sshUser' ) {
  446. if( $cmd eq "set" && $attrVal ) {
  447. $attr{$name}{$attrName} = $attrVal;
  448. } else {
  449. delete $attr{$name}{$attrName};
  450. UnifiVideo_killLogWatcher($hash);
  451. }
  452. UnifiVideo_Connect($hash);
  453. } elsif( $attrName eq 'apiKey' ) {
  454. if( $cmd eq "set" && $attrVal ) {
  455. return if( $attrVal =~ m/^crypt:/ );
  456. $attrVal = UnifiVideo_encrypt($attrVal);
  457. if( $orig ne $attrVal ) {
  458. $attr{$name}{$attrName} = $attrVal;
  459. UnifiVideo_Connect($hash);
  460. return "stored obfuscated apiKey";
  461. }
  462. }
  463. }
  464. if( $cmd eq 'set' ) {
  465. } else {
  466. delete $attr{$name}{$attrName};
  467. }
  468. return;
  469. }
  470. 1;
  471. =pod
  472. =item summary Module to integrate FHEM with UnifiVideo
  473. =item summary_DE Modul zur Integration von FHEM mit UnifiVideo
  474. =begin html
  475. <a name="UnifiVideo"></a>
  476. <h3>UnifiVideo</h3>
  477. <ul>
  478. Module to integrate UnifiVideo devices with FHEM.<br><br>
  479. define &lt;name&gt; UnifiVideo &lt;ip&gt; [&lt;apiKey&gt;] <br><br>
  480. Notes:
  481. <ul>
  482. <li>JSON has to be installed on the FHEM host.</li>
  483. <li>create nvr api key: admin-&gt;my account-&gt;api access</li>
  484. <li><code>define <name> webLink htmlCode {UnifiVideo_2html('&lt;nvr&gt;','&lt;cam&gt;[,&lt;cam2&gt;,..]'[,&lt;width&gt;])}</code></li>
  485. </ul><br>
  486. <a name="UnifiVideo_Set"></a>
  487. <b>Set</b>
  488. <ul>
  489. <li>snapshot cam=&lt;cam&gt; width=&lt;width&gt; fileName=&lt;fileName&gt;<br>
  490. takes a snapshot from &lt;cam&gt; with optional &lt;width&gt; and stores it with the optional &lt;fileName&gt;<br>
  491. &lt;cam&gt; can be the number of the cammera, its id or a regex that is matched against the name.
  492. </li>
  493. <li>reconnect<br>
  494. </li>
  495. </ul>
  496. <a name="UnifiVideo_Get"></a>
  497. <b>Get</b>
  498. <ul>
  499. <li>apiKey<br>
  500. shows the configured apiKey.</li>
  501. </ul>
  502. <a name="UnifiVideo_Attr"></a>
  503. <b>Attr</b>
  504. <ul>
  505. <li>filePath<br>
  506. path to store the snapshot images to. default: .../www/snapshots
  507. </li>
  508. <li>apiKey<br>
  509. apiKey to use for nvr access
  510. </li>
  511. <li>ssh_user<br>
  512. ssh user for nvr logfile access. used to fhem events after motion detection.
  513. </li>
  514. </ul>
  515. </ul><br>
  516. =end html
  517. =cut