70_VolumeLink.pm 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433
  1. ###############################################################################
  2. # $Id: 70_VolumeLink.pm 12009 2016-08-20 10:17:07Z rapster $
  3. package main;
  4. use strict;
  5. use warnings;
  6. use POSIX;
  7. use HttpUtils;
  8. use Time::HiRes qw(gettimeofday time);
  9. use Scalar::Util;
  10. ###############################################################################
  11. sub VolumeLink_Initialize($$) {
  12. my ($hash) = @_;
  13. $hash->{DefFn} = "VolumeLink_Define";
  14. $hash->{UndefFn} = "VolumeLink_Undef";
  15. $hash->{SetFn} = "VolumeLink_Set";
  16. $hash->{AttrFn} = 'VolumeLink_Attr';
  17. $hash->{AttrList} = "disable:1,0 "
  18. ."ampInputReading "
  19. ."ampInputReadingVal "
  20. ."ampVolumeReading "
  21. ."ampVolumeCommand "
  22. ."ampMuteReading "
  23. ."ampMuteReadingOnVal "
  24. ."ampMuteReadingOffVal "
  25. ."ampMuteCommand "
  26. ."volumeRegexPattern "
  27. ."muteRegexPattern "
  28. ."httpNoShutdown:1,0 "
  29. .$readingFnAttributes;
  30. }
  31. ###############################################################################
  32. sub VolumeLink_Define($$) {
  33. my ($hash, $def) = @_;
  34. my @a = split("[ \t][ \t]*", $def);
  35. return "Wrong syntax: use define <name> VolumeLink <interval> <url> <ampDevice> [<timeout> [<httpErrorLoglevel> [<httpLoglevel>]]]" if(int(@a) < 5);
  36. return "Wrong syntax: <interval> is not a number!" if(!looks_like_number($a[2]));
  37. return "Wrong syntax: <interval> too small, must be at least 0.01" if($a[2] < 0.01);
  38. return "Wrong syntax: <timeout> is not a number!" if($a[5] && !looks_like_number($a[5]));
  39. return "Wrong syntax: <timeout> too small, must be at least 0.01" if($a[5] && $a[5] < 0.01);
  40. return "Wrong syntax: <ampDevice> not defined! Define '$a[4]' first." if(!defined$defs{$a[4]});
  41. my $name = $a[0];
  42. %$hash = ( %$hash,
  43. STARTED => $hash->{STARTED} || 0,
  44. interval => $a[2],
  45. url => $a[3],
  46. ampDevice => $a[4],
  47. timeout => $a[5] || 0.5,
  48. httpErrorLoglevel => $a[6] || 4,
  49. httpLoglevel => $a[7] || 5,
  50. httpNoShutdown => ( defined($attr{$name}->{httpNoShutdown}) ) ? $attr{$name}->{httpNoShutdown} : 1,
  51. volumeRegexPattern => $attr{$name}->{volumeRegexPattern} || 'current":\s*(\d+)',
  52. muteRegexPattern => $attr{$name}->{muteRegexPattern} || 'muted":\s*(\w+|\d+)',
  53. ampInputReading => ( defined($attr{$name}->{ampInputReading}) ) ? $attr{$name}->{ampInputReading} : 'currentTitle',
  54. ampInputReadingVal => ( defined($attr{$name}->{ampInputReadingVal}) ) ? $attr{$name}->{ampInputReadingVal} : 'SPDIF-Wiedergabe|^$',
  55. ampVolumeReading => $attr{$name}->{ampVolumeReading} || 'Volume',
  56. ampVolumeCommand => $attr{$name}->{ampVolumeCommand} || 'Volume',
  57. ampMuteReading => $attr{$name}->{ampMuteReading} || 'Mute',
  58. ampMuteReadingOnVal => ( defined($attr{$name}->{ampMuteReadingOnVal}) ) ? $attr{$name}->{ampMuteReadingOnVal} : 1,
  59. ampMuteReadingOffVal => ( defined($attr{$name}->{ampMuteReadingOffVal}) ) ? $attr{$name}->{ampMuteReadingOffVal} : 0,
  60. ampMuteCommand => $attr{$name}->{ampMuteCommand} || 'Mute'
  61. );
  62. $hash->{httpParams} = {
  63. HTTP_ERROR_COUNT => 0,
  64. fastRetryInterval => 0.1,
  65. hash => $hash,
  66. url => $hash->{url},
  67. timeout => $hash->{timeout},
  68. noshutdown => $hash->{httpNoShutdown},
  69. loglevel => $hash->{httpLoglevel},
  70. errorLoglevel => $hash->{httpErrorLoglevel},
  71. method => 'GET',
  72. callback => \&VolumeLink_ReceiveCommand
  73. };
  74. readingsSingleUpdate($hash,'state','off',1) if($hash->{STARTED} == 0 && ReadingsVal($name,'state','') ne 'off');
  75. Log3 $name, 3, "$name: Defined with interval:$hash->{interval}, url:$hash->{url}, timeout:$hash->{timeout}, ampDevice:$hash->{ampDevice}";
  76. return undef;
  77. }
  78. ###############################################################################
  79. sub VolumeLink_Undef($$) {
  80. my ($hash,$arg) = @_;
  81. $hash->{STARTED} = 0;
  82. RemoveInternalTimer($hash);
  83. Log3 $hash->{NAME}, 3, "$hash->{NAME}: STOPPED";
  84. return undef;
  85. }
  86. ###############################################################################
  87. sub VolumeLink_Set($@) {
  88. my ($hash,@a) = @_;
  89. return "\"set $hash->{NAME}\" needs at least an argument" if ( @a < 2 );
  90. my ($name,$setName,$setVal) = @a;
  91. if (AttrVal($name, "disable", 0)) {
  92. Log3 $name, 5, "$name: set called with $setName but device is disabled" if ($setName ne "?");
  93. return undef;
  94. }
  95. Log3 $name, 5, "$name: set called with $setName " . ($setVal ? $setVal : "") if ($setName ne "?");
  96. if($setName !~ /on|off/) {
  97. return "Unknown argument $setName, choose one of on:noArg off:noArg";
  98. } else {
  99. Log3 $name, 4, "VolumeLink: set $name $setName";
  100. if ($setName eq 'on') {
  101. if($hash->{STARTED} == 0) {
  102. $hash->{STARTED} = 1;
  103. Log3 $name, 3, "$name: STARTED";
  104. readingsSingleUpdate($hash,"state",$setName,1);
  105. VolumeLink_SendCommand($hash);
  106. }
  107. }
  108. elsif ($setName eq 'off') {
  109. if($hash->{STARTED} == 1) {
  110. $hash->{STARTED} = 0;
  111. RemoveInternalTimer($hash);
  112. Log3 $name, 3, "$name: STOPPED";
  113. readingsSingleUpdate($hash,"state",$setName,1);
  114. }
  115. }
  116. }
  117. return undef;
  118. }
  119. ###############################################################################
  120. sub VolumeLink_Attr(@) {
  121. my ($cmd,$name,$attr_name,$attr_value) = @_;
  122. if($cmd eq "set") {
  123. if($attr_name eq "disable" && $attr_value == 1) {
  124. CommandSet(undef, $name.' off');
  125. }
  126. $defs{$name}->{ampInputReading} = $attr_value if($attr_name eq 'ampInputReading');
  127. $defs{$name}->{ampInputReadingVal} = $attr_value if($attr_name eq 'ampInputReadingVal');
  128. $defs{$name}->{ampVolumeReading} = $attr_value if($attr_name eq 'ampVolumeReading');
  129. $defs{$name}->{ampVolumeCommand} = $attr_value if($attr_name eq 'ampVolumeCommand');
  130. $defs{$name}->{ampMuteReading} = $attr_value if($attr_name eq 'ampMuteReading');
  131. $defs{$name}->{ampMuteReadingOnVal} = $attr_value if($attr_name eq 'ampMuteReadingOnVal');
  132. $defs{$name}->{ampMuteReadingOffVal} = $attr_value if($attr_name eq 'ampMuteReadingOffVal');
  133. $defs{$name}->{ampMuteCommand} = $attr_value if($attr_name eq 'ampMuteCommand');
  134. $defs{$name}->{volumeRegexPattern} = $attr_value if($attr_name eq 'volumeRegexPattern');
  135. $defs{$name}->{muteRegexPattern} = $attr_value if($attr_name eq 'muteRegexPattern');
  136. $defs{$name}->{httpNoShutdown} = $attr_value if($attr_name eq 'httpNoShutdown');
  137. if($attr_name eq 'httpNoShutdown') {
  138. $defs{$name}->{httpNoShutdown} = $attr_value;
  139. $defs{$name}->{httpParams}->{noshutdown} = $defs{$name}->{httpNoShutdown};
  140. }
  141. }
  142. elsif($cmd eq "del") {
  143. $defs{$name}->{ampInputReading} = 'currentTitle' if($attr_name eq 'ampInputReading');
  144. $defs{$name}->{ampInputReadingVal} = 'SPDIF-Wiedergabe|^$' if($attr_name eq 'ampInputReadingVal');
  145. $defs{$name}->{ampVolumeReading} = 'Volume' if($attr_name eq 'ampVolumeReading');
  146. $defs{$name}->{ampVolumeCommand} = 'Volume' if($attr_name eq 'ampVolumeCommand');
  147. $defs{$name}->{ampMuteReading} = 'Mute' if($attr_name eq 'ampMuteReading');
  148. $defs{$name}->{ampMuteReadingOnVal} = 1 if($attr_name eq 'ampMuteReadingOnVal');
  149. $defs{$name}->{ampMuteReadingOffVal} = 0 if($attr_name eq 'ampMuteReadingOffVal');
  150. $defs{$name}->{ampMuteCommand} = 'Mute' if($attr_name eq 'ampMuteCommand');
  151. $defs{$name}->{volumeRegexPattern} = 'current":\s*(\d+)' if($attr_name eq 'volumeRegexPattern');
  152. $defs{$name}->{muteRegexPattern} = 'muted":\s*(\w+|\d+)' if($attr_name eq 'muteRegexPattern');
  153. if($attr_name eq 'httpNoShutdown') {
  154. $defs{$name}->{httpNoShutdown} = 1;
  155. $defs{$name}->{httpParams}->{noshutdown} = $defs{$name}->{httpNoShutdown};
  156. }
  157. }
  158. return undef;
  159. }
  160. ###############################################################################
  161. sub VolumeLink_SendCommand($) {
  162. my ($hash) = @_;
  163. Log3 $hash->{NAME}, 5, "$hash->{NAME}: SendCommand - executed";
  164. HttpUtils_NonblockingGet($hash->{httpParams});
  165. return undef;
  166. }
  167. ###############################################################################
  168. sub VolumeLink_ReceiveCommand($) {
  169. my ($param, $err, $data) = @_;
  170. my $name = $param->{hash}->{NAME};
  171. my $interval = $param->{hash}->{interval};
  172. Log3 $name, 5, "$name: ReceiveCommand - executed";
  173. if($err ne "") {
  174. if($interval > $param->{fastRetryInterval} && $err =~ /timed.out/ && $param->{HTTP_ERROR_COUNT} < 3) {
  175. $interval = $param->{fastRetryInterval};
  176. $param->{HTTP_ERROR_COUNT}++;
  177. readingsSingleUpdate($param->{hash},'lastHttpError',"$err #$param->{HTTP_ERROR_COUNT} of 3, do fast-retry in $interval sec.",0);
  178. Log3 $name, $param->{errorLoglevel}, "$name: Error while requesting ".$param->{url}." - $err - Fast-retry #$param->{HTTP_ERROR_COUNT} of 3 in $interval seconds.";
  179. }
  180. else {
  181. readingsSingleUpdate($param->{hash},'lastHttpError',"$err, retry in $interval sec.",0);
  182. Log3 $name, $param->{errorLoglevel}, "$name: Error while requesting ".$param->{url}." - $err - Retry in $interval seconds.";
  183. }
  184. }
  185. elsif($data ne "") {
  186. Log3 $name, $param->{loglevel}, "$name: url ".$param->{url}." returned: $data";
  187. $param->{HTTP_ERROR_COUNT} = 0;
  188. my ($vol) = $data =~ /$param->{hash}->{volumeRegexPattern}/si;
  189. my ($mute) = $data =~ /$param->{hash}->{muteRegexPattern}/si;
  190. if (!defined($vol)) {$vol = '';}
  191. if (!defined($mute)) {$mute = '';}
  192. Log3 $name, 5, "$name - volumeRegexPattern: m/$param->{hash}->{volumeRegexPattern}/si - returned:'$vol'";
  193. Log3 $name, 5, "$name - muteRegexPattern: m/$param->{hash}->{muteRegexPattern}/si - returned:'$mute'";
  194. if(looks_like_number($vol)) {
  195. if($mute =~ /true|false|0|1/i) {
  196. $vol = int($vol);
  197. Log3 $name, 5, "$name: Values O.K. - currentVolume:'$vol' - muted:'$mute' - Set it now...";
  198. readingsBeginUpdate($param->{hash});
  199. readingsBulkUpdate($param->{hash}, 'volume', $vol );
  200. readingsBulkUpdate($param->{hash}, 'mute', $mute );
  201. readingsEndUpdate($param->{hash}, 0);
  202. if( !defined($defs{$param->{hash}->{ampDevice}}) ) {
  203. Log3 $name, 1, "$name: FAILURE, configured <ampDevice> '$param->{hash}->{ampDevice}' is not defined. End now...";
  204. CommandSet(undef, $name.' off');
  205. return;
  206. }
  207. my $ampMute = ReadingsVal($param->{hash}->{ampDevice},$param->{hash}->{ampMuteReading},'N/A');
  208. my $ampVol = ReadingsVal($param->{hash}->{ampDevice},$param->{hash}->{ampVolumeReading},'N/A');
  209. my $ampTitle = ( $param->{hash}->{ampInputReading} ) ? ReadingsVal($param->{hash}->{ampDevice},$param->{hash}->{ampInputReading},'N/A') : 0;
  210. Log3 $name, 5, "$name: Fetched amp-readings - ampMute:'$ampMute' - ampVol:'$ampVol' - ampInput:'$ampTitle'";
  211. if($ampMute eq 'N/A' || $ampVol eq 'N/A' || $ampTitle eq 'N/A') {
  212. Log3 $name, 1, "$name: FAILURE, can not fetch an amp-reading! End now... - ampMute:'$ampMute' - ampVol:'$ampVol' - ampInput:'$ampTitle' ";
  213. CommandSet(undef, $name.' off');
  214. return;
  215. }
  216. if($ampTitle =~ /$param->{hash}->{ampInputReadingVal}/i || !$param->{hash}->{ampInputReading}) {
  217. if($vol ne $ampVol) {
  218. Log3 $name, 5, "$name: Set Volume on ampDevice '$param->{hash}->{ampDevice}' - newVolume:'$vol' - oldVolume:'$ampVol'.";
  219. CommandSet(undef, $param->{hash}->{ampDevice}.' '.$param->{hash}->{ampVolumeCommand}.' '.$vol);
  220. }
  221. if($mute =~ /true|1/i && $ampMute eq $param->{hash}->{ampMuteReadingOffVal}) {
  222. Log3 $name, 5, "$name: Set MuteOn on ampDevice '$param->{hash}->{ampDevice}'.";
  223. CommandSet(undef, $param->{hash}->{ampDevice}.' '.$param->{hash}->{ampMuteCommand}.' '.$param->{hash}->{ampMuteReadingOnVal});
  224. }
  225. if($mute =~ /false|0/i && $ampMute eq $param->{hash}->{ampMuteReadingOnVal}) {
  226. Log3 $name, 5, "$name: Set MuteOff on ampDevice '$param->{hash}->{ampDevice}'.";
  227. CommandSet(undef, $param->{hash}->{ampDevice}.' '.$param->{hash}->{ampMuteCommand}.' '.$param->{hash}->{ampMuteReadingOffVal});
  228. }
  229. }else {
  230. Log3 $name, 5, "$name: current amp-input: '$ampTitle' not match configured input.' - Skip setting volume in this turn...";
  231. }
  232. }
  233. else {
  234. Log3 $name, 1, "$name: FAILURE, muteRegexPattern 'm/$param->{hash}->{muteRegexPattern}/si' delivers bad mute-state! Must be 0, 1, true, or false. End now... - returned:'$mute'";
  235. CommandSet(undef, $name.' off');
  236. return;
  237. }
  238. }
  239. else {
  240. Log3 $name, 1, "$name: FAILURE, volumeRegexPattern 'm/$param->{hash}->{volumeRegexPattern}/si' delivers bad volume-level (Not a number)! End now... - returned:'$vol'";
  241. CommandSet(undef, $name.' off');
  242. return;
  243. }
  244. }
  245. if($param->{hash}->{STARTED} == 1) {
  246. InternalTimer(time()+$interval, 'VolumeLink_SendCommand', $param->{hash}, 0);
  247. }
  248. return undef;
  249. }
  250. ###############################################################################
  251. 1;
  252. =pod
  253. =item device
  254. =item summary Bind volume of a physical device to a fhem device.
  255. =item summary_DE Verbindet die Lautstaerke eines physischen Geraets mit einem Fhem Geraet.
  256. =begin html
  257. <a name="VolumeLink"></a>
  258. <h3>VolumeLink</h3>
  259. <ul>
  260. VolumeLink links the volume-level &amp; mute-state from a physical device (e.g. a Philips-TV) with the volume &amp; mute control of a fhem device (e.g. a SONOS-Playbar, Onkyo, Yamaha or Denon Receiver, etc.).
  261. <br><br>
  262. <h4>Define</h4>
  263. <ul>
  264. <code>define &lt;name&gt; VolumeLink &lt;interval&gt; &lt;url&gt; &lt;ampDevice&gt; [&lt;timeout&gt; [&lt;httpErrorLoglevel&gt; [&lt;httpLoglevel&gt;]]]</code>
  265. <br><br>
  266. <br>
  267. &lt;interval&gt;:
  268. <ul>
  269. <code>interval to fetch current volume &amp; mute level from physical-device.</code><br>
  270. </ul>
  271. &lt;url&gt;:
  272. <ul>
  273. <code>url to fetch volume &amp; mute level, see Example below. (Example applies to many Philips TV's)</code><br>
  274. </ul>
  275. &lt;ampDevice&gt;:
  276. <ul>
  277. <code>the target fhem-device.</code><br>
  278. </ul>
  279. [&lt;timeout&gt;]:
  280. <ul>
  281. <code>optional: timeout of a http-get. default: 0.5 seconds</code><br>
  282. </ul>
  283. [&lt;httpErrorLoglevel&gt;]:
  284. <ul>
  285. <code>optional: loglevel of http-errors. default: 4</code><br>
  286. </ul>
  287. [&lt;httpLoglevel&gt;]:
  288. <ul>
  289. <code>optional: loglevel of http-messages. default: 5</code><br>
  290. </ul>
  291. </ul>
  292. <br>
  293. <h4>Example</h4>
  294. <ul>
  295. <code>define tvVolume_LivingRoom VolumeLink 0.2 http://192.168.1.156:1925/5/audio/volume Sonos_LivingRoom</code><br>
  296. <code>set tvVolume_LivingRoom on</code><br>
  297. <br>
  298. Note:<br>
  299. - This example will work out of the box with many Philips TV's and a SONOS-Playbar as fhem-device.<br>
  300. - Pre 2014 Philips TV's use another protocoll, which can be accessed on http://&lt;ip&gt;/1/audio/volume
  301. </ul>
  302. <br>
  303. <h4>Set</h4>
  304. <ul>
  305. <code>set &lt;name&gt; &lt;on|off&gt</code><br>
  306. <br>
  307. Set on or off, to start or to stop.
  308. </ul>
  309. <br>
  310. <h4>Get</h4> <ul>N/A</ul><br>
  311. <h4>Attributes</h4>
  312. <ul>
  313. Note:<br>
  314. - All Attributes takes effect immediately.<br>
  315. - The default value of volumeRegexPattern &amp; muteRegexPattern applies to many Philips-TV's, otherwise it must be configured.<br>
  316. - The default values of amp* applies to a SONOS-Playbar, otherwise it must be configured.<br>
  317. - If you don't receive a result from url, or the lastHttpErrorMessage shows every time 'timed out', try setting attribute 'httpNoShutdown' to 0.<br>
  318. <br>
  319. <li>disable &lt;1|0&gt;<br>
  320. With this attribute you can disable the whole module. <br>
  321. If set to 1 the module will be stopped and no volume will be fetched from physical-device or transfer to the amplifier-device. <br>
  322. If set to 0 you can start the module again with: set &lt;name&gt; on.</li>
  323. <li>httpNoShutdown &lt;1|0&gt;<br>
  324. If set to 0 VolumeLink will tell the http-server to explicit close the connection.<br>
  325. <i>Default: 1</i>
  326. </li>
  327. <li>ampInputReading &lt;value&gt;<br>
  328. Name of the Input-Reading on amplifier-device<br>
  329. To disable the InputCheck if your amplifier-device does not support this, set this attribute to 0.<br>
  330. <i>Default (which applies to SONOS-Player's): currentTitle</i></li>
  331. <li>ampInputReadingVal &lt;RegEx&gt;<br>
  332. RegEx for the Reading value of the corresponding Input-Channel on amplifier-device<br>
  333. <i>Default (which applies to a SONOS-Playbar's SPDIF-Input and if no Input is selected): SPDIF-Wiedergabe|^$</i></li>
  334. <li>ampVolumeReading &lt;value&gt;<br>
  335. Name of the Volume-Reading on amplifier-device<br>
  336. <i>Default: Volume</i></li>
  337. <li>ampVolumeCommand &lt;value&gt;<br>
  338. Command to set the volume on amplifier device<br>
  339. <i>Default: Volume</i></li>
  340. <li>ampMuteReading &lt;value&gt;<br>
  341. Name of the Mute-Reading on amplifier-device<br>
  342. <i>Default: Mute</i></li>
  343. <li>ampMuteReadingOnVal &lt;value&gt;<br>
  344. Reading value if muted<br>
  345. <i>Default: 1</i></li>
  346. <li>ampMuteReadingOffVal &lt;value&gt;<br>
  347. Reading value if not muted<br>
  348. <i>Default: 0</i></li>
  349. <li>ampMuteCommand &lt;value&gt;<br>
  350. Command to mute the amplifier device<br>
  351. <i>Default: Mute</i></li>
  352. <li>volumeRegexPattern &lt;RegEx&gt;<br>
  353. RegEx which is applied to url return data. Must return a number for volume-level. <br>
  354. <i>Default (which applies to many Phlips-TV's): current&quot;:&#92;s*(&#92;d+)</i></li>
  355. <li>muteRegexPattern &lt;RegEx&gt;<br>
  356. RegEx which is applied to url return data. Must return true, false, 1 or 0 as mute-state. <br>
  357. <i>Default (which applies to many Phlips-TV's): muted&quot;:&#92;s*(&#92;w+|&#92;d+)</i></li>
  358. </ul><br>
  359. <h4>Readings</h4>
  360. <ul>
  361. Note: All VolumeLink Readings except of 'state' does not generate events!<br>
  362. <br>
  363. <li>lastHttpError<br>
  364. The last HTTP-Error will be recorded in this reading.<br>
  365. Define httpErrorLoglevel, httpLoglevel or attribute <a href="#verbose">verbose</a> for more information.<br>
  366. Note: Attr <a href="#verbose">verbose</a> will not output all HTTP-Messages, define httpLoglevel for this.</li>
  367. <li>mute<br>
  368. The current mute-state fetched from physical device.</li>
  369. <li>volume<br>
  370. The current volume-level fetched from physical device.</li>
  371. <li>state<br>
  372. on if VolumeLink is running, off if VolumeLink is stopped.</li>
  373. </ul>
  374. <br>
  375. </ul>
  376. =end html
  377. =cut