98_DLNARenderer.pm 60 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761
  1. ############################################################################
  2. # Author: dominik.karall@gmail.com
  3. # $Id: 98_DLNARenderer.pm 15836 2018-01-09 21:01:49Z dominik $
  4. #
  5. # v2.0.7 - 20180108
  6. # - FEATURE: support ignoredIPs and usedonlyIPs attribute
  7. #
  8. # v2.0.6 - 20171209
  9. # - FEATURE: support acceptedUDNs for UDN whitelisting (thx@MichaelT!)
  10. # - BUGFIX: fix renew subscriptions errors on offline devices
  11. # - BUGFIX: fix renew warnings, now only on log level 5 (thx@mumpitzstuff!)
  12. #
  13. # v2.0.5 - 20170430
  14. # - BUGFIX: fix "readings not updated"
  15. #
  16. # v2.0.4 - 20170421
  17. # - FEATURE: support $readingFnAttributes
  18. # - BUGFIX: fix some freezes
  19. # - BUGFIX: retry UPnP call 3 times if it fails (error 500)
  20. #
  21. # v2.0.3 - 20160918
  22. # - BUGFIX: fixed SyncPlay for CaskeId players
  23. #
  24. # v2.0.2 - 20160913
  25. # - BUGFIX: fixed pauseToggle (thx@MattG)
  26. # - BUGFIX: fixed next/previous (thx@MattG)
  27. #
  28. # v2.0.1 - 20160725
  29. # - FEATURE: support DIDL-Lite in channel_X attribute (thx@Weissbrotgrill)
  30. # - FEATURE: automatically generate DIDL-Lite based on URI (thx@Weissbrotgrill)
  31. # - CHANGE: update CommandRef perl library requirements
  32. # - BUGFIX: fix ignoreUDNs crash when device gets removed
  33. #
  34. # v2.0.0 - 20160718
  35. # - CHANGE: first official release within fhem repository
  36. # - BUGFIX: support device events without / at the end of the xmlns (thx@MichaelT)
  37. # - FEATURE: support defaultRoom attribute, defines the room to which new devices are assigned
  38. #
  39. # v2.0.0 RC5 - 20160614
  40. # - BUGFIX: support events from devices with wrong serviceId
  41. # - BUGFIX: fix perl warning on startup
  42. # - BUGFIX: fix error if LastChange event is empty
  43. #
  44. # v2.0.0 RC4 - 20160613
  45. # - FEATURE: support devices with wrong serviceId
  46. # - BUGFIX: fix crash during stereo mode update for caskeid players
  47. # - FEATURE: add stereoPairName reading
  48. # - CHANGE: add version string to main device internals
  49. # - BUGFIX: fix error when UPnP method is not implemented
  50. # - FEATURE: identify stereo support (reading: stereoSupport)
  51. #
  52. # v2.0.0 RC3 - 20160609
  53. # - BUGFIX: check correct number of params for all commands
  54. # - BUGFIX: fix addUnitToSession/removeUnitFromSession for MUNET/Caskeid devices
  55. # - BUGFIX: support devices with non-standard UUIDs
  56. # - CHANGE: use BlockingCall for subscription renewal
  57. # - CHANGE: remove ignoreUDNs attribute from play devices
  58. # - CHANGE: remove multiRoomGroups attribute from main device
  59. # - CHANGE: split stereoDevices reading into stereoLeft/stereoRight
  60. # - FEATURE: support multiRoomVolume to change volume of all group speakers e.g.
  61. # set <name> multiRoomVolume +10
  62. # set <name> multiRoomVolume 25
  63. # - FEATURE: support channel_01-10 attribute
  64. # attr <name> channel_01 http://... (save URI to channel_01)
  65. # set <name> channel 1 (play channel_01)
  66. # - FEATURE: support speak functionality via Google Translate
  67. # set <name> speak "This is a test."
  68. # attr <name> ttsLanguage de
  69. # set <name> speak "Das ist ein Test."
  70. # - FEATURE: automatically retrieve stereo mode from speakers and update stereoId/Left/Right readings
  71. # - FEATURE: support mute
  72. # set <name> mute on/off
  73. #
  74. # v2.0.0 RC2 - 20160510
  75. # - BUGFIX: fix multiroom for MUNET/Caskeid devices
  76. #
  77. # v2.0.0 RC1 - 20160509
  78. # - CHANGE: change state to offline/playing/stopped/paused/online
  79. # - CHANGE: removed on/off devstateicon on creation due to changed state values
  80. # - CHANGE: play is NOT setting AVTransport any more
  81. # - CHANGE: code cleanup
  82. # - CHANGE: handle socket via fhem main loop instead of InternalTimer
  83. # - BUGFIX: do not create new search objects every 30 minutes
  84. # - FEATURE: support pauseToggle
  85. # - FEATURE: support SetExtensions (on-for-timer, off-for-timer, ...)
  86. # - FEATURE: support relative volume changes (e.g. set <device> volume +10)
  87. #
  88. # v2.0.0 BETA3 - 20160504
  89. # - BUGFIX: XML parsing error "NOT_IMPLEMENTED"
  90. # - CHANGE: change readings to lowcaseUppercase format
  91. # - FEATURE: support pause
  92. # - FEATURE: support seek REL_TIME
  93. # - FEATURE: support next/prev
  94. #
  95. # v2.0.0 BETA2 - 20160403
  96. # - FEATURE: support events from DLNA devices
  97. # - FEATURE: support caskeid group definitions
  98. # set <name> saveGroupAs Bad
  99. # set <name> loadGroup Bad
  100. # - FEATURE: support caskeid stereo mode
  101. # set <name> stereo MUNET1 MUNET2 MunetStereoPaar
  102. # set <name> standalone
  103. # - CHANGE: use UPnP::ControlPoint from FHEM library
  104. # - BUGFIX: fix presence status
  105. #
  106. # v2.0.0 BETA1 - 20160321
  107. # - FEATURE: autodiscover and autocreate DLNA devices
  108. # just use "define dlnadevices DLNARenderer" and wait 2 minutes
  109. # - FEATURE: support Caskeid (e.g. MUNET devices) with following commands
  110. # set <name> playEverywhere
  111. # set <name> stopPlayEverywhere
  112. # set <name> addUnit <UNIT>
  113. # set <name> removeUnit <UNIT>
  114. # set <name> enableBTCaskeid
  115. # set <name> disableBTCaskeid
  116. # - FEATURE: display multiroom speakers in multiRoomUnits reading
  117. # - FEATURE: automatically set alias for friendlyname
  118. # - FEATURE: automatically set webCmd volume
  119. # - FEATURE: automatically set devStateIcon audio icons
  120. # - FEATURE: ignoreUDNs attribute in main
  121. # - FEATURE: scanInterval attribute in main
  122. #
  123. # DLNA Module to play given URLs on a DLNA Renderer
  124. # and control their volume. Just define
  125. # define dlnadevices DLNARenderer
  126. # and look for devices in Unsorted section after 2 minutes.
  127. #
  128. #TODO
  129. # - speak: support continue stream after speak finished
  130. # - redesign multiroom functionality (virtual devices: represent the readings of master device
  131. # and send the commands only to the master device (except volume?)
  132. # automatically create group before playing
  133. # - use bulk update for readings
  134. #
  135. ############################################################################
  136. package main;
  137. use strict;
  138. use warnings;
  139. use Blocking;
  140. use SetExtensions;
  141. use HTML::Entities;
  142. use XML::Simple;
  143. use Data::Dumper;
  144. use Data::UUID;
  145. use LWP::UserAgent;
  146. #get UPnP::ControlPoint loaded properly
  147. my $gPath = '';
  148. BEGIN {
  149. $gPath = substr($0, 0, rindex($0, '/'));
  150. }
  151. if (lc(substr($0, -7)) eq 'fhem.pl') {
  152. $gPath = $attr{global}{modpath}.'/FHEM';
  153. }
  154. use lib ($gPath.'/lib', $gPath.'/FHEM/lib', './FHEM/lib', './lib', './FHEM', './', '/usr/local/FHEM/share/fhem/FHEM/lib');
  155. use UPnP::ControlPoint;
  156. sub DLNARenderer_Initialize($) {
  157. my ($hash) = @_;
  158. $hash->{SetFn} = "DLNARenderer_Set";
  159. $hash->{DefFn} = "DLNARenderer_Define";
  160. $hash->{ReadFn} = "DLNARenderer_Read";
  161. $hash->{UndefFn} = "DLNARenderer_Undef";
  162. $hash->{AttrFn} = "DLNARenderer_Attribute";
  163. $hash->{AttrList} = "ignoredIPs usedonlyIPs ".$readingFnAttributes;
  164. }
  165. sub DLNARenderer_Attribute {
  166. my ($mode, $devName, $attrName, $attrValue) = @_;
  167. #ignoreUDNs, multiRoomGroups, channel_01-10
  168. if($mode eq "set") {
  169. } elsif($mode eq "del") {
  170. }
  171. return undef;
  172. }
  173. sub DLNARenderer_Define($$) {
  174. my ($hash, $def) = @_;
  175. my @param = split("[ \t][ \t]*", $def);
  176. #init caskeid clients for multiroom
  177. $hash->{helper}{caskeidClients} = "";
  178. $hash->{helper}{caskeid} = 0;
  179. if(@param < 3) {
  180. #main
  181. $hash->{UDN} = 0;
  182. my $VERSION = "v2.0.7";
  183. $hash->{VERSION} = $VERSION;
  184. Log3 $hash, 3, "DLNARenderer: DLNA Renderer $VERSION";
  185. DLNARenderer_setupControlpoint($hash);
  186. DLNARenderer_startDlnaRendererSearch($hash);
  187. readingsSingleUpdate($hash,"state","initialized",1);
  188. addToDevAttrList($hash->{NAME}, "ignoreUDNs");
  189. addToDevAttrList($hash->{NAME}, "acceptedUDNs");
  190. addToDevAttrList($hash->{NAME}, "defaultRoom");
  191. return undef;
  192. }
  193. #device specific
  194. my $name = shift @param;
  195. my $type = shift @param;
  196. my $udn = shift @param;
  197. $hash->{UDN} = $udn;
  198. readingsSingleUpdate($hash,"presence","offline",1);
  199. readingsSingleUpdate($hash,"state","offline",1);
  200. addToDevAttrList($hash->{NAME}, "multiRoomGroups");
  201. addToDevAttrList($hash->{NAME}, "ttsLanguage");
  202. addToDevAttrList($hash->{NAME}, "channel_01");
  203. addToDevAttrList($hash->{NAME}, "channel_02");
  204. addToDevAttrList($hash->{NAME}, "channel_03");
  205. addToDevAttrList($hash->{NAME}, "channel_04");
  206. addToDevAttrList($hash->{NAME}, "channel_05");
  207. addToDevAttrList($hash->{NAME}, "channel_06");
  208. addToDevAttrList($hash->{NAME}, "channel_07");
  209. addToDevAttrList($hash->{NAME}, "channel_08");
  210. addToDevAttrList($hash->{NAME}, "channel_09");
  211. addToDevAttrList($hash->{NAME}, "channel_10");
  212. return undef;
  213. }
  214. sub DLNARenderer_Undef($) {
  215. my ($hash) = @_;
  216. RemoveInternalTimer($hash);
  217. return undef;
  218. }
  219. sub DLNARenderer_Read($) {
  220. my ($hash) = @_;
  221. my $name = $hash->{NAME};
  222. my $phash = $hash->{phash};
  223. my $cp = $phash->{helper}{controlpoint};
  224. eval {
  225. $cp->handleOnce($hash->{CD});
  226. };
  227. if($@) {
  228. Log3 $hash, 3, "DLNARenderer: handleOnce failed, $@";
  229. }
  230. return undef;
  231. }
  232. sub DLNARenderer_Set($@) {
  233. my ($hash, $name, @params) = @_;
  234. my $dev = $hash->{helper}{device};
  235. # check parameters
  236. return "no set value specified" if(int(@params) < 1);
  237. my $ctrlParam = shift(@params);
  238. # check device presence
  239. if ($ctrlParam ne "?" and (!defined($dev) or ReadingsVal($hash->{NAME}, "presence", "") eq "offline")) {
  240. return "DLNARenderer: Currently searching for device...";
  241. }
  242. #get quoted text from params
  243. my $blankParams = join(" ", @params);
  244. my @params2;
  245. while($blankParams =~ /"?((?<!")\S+(?<!")|[^"]+)"?\s*/g) {
  246. push(@params2, $1);
  247. }
  248. @params = @params2;
  249. my $set_method_mapping = {
  250. volume => {method => \&DLNARenderer_volume, args => 1, argdef => "slider,0,1,100"},
  251. mute => {method => \&DLNARenderer_mute, args => 1, argdef => "on,off"},
  252. pause => {method => \&DLNARenderer_upnpPause, args => 0},
  253. pauseToggle => {method => \&DLNARenderer_pauseToggle, args => 0},
  254. play => {method => \&DLNARenderer_play, args => 0},
  255. next => {method => \&DLNARenderer_upnpNext, args => 0},
  256. previous => {method => \&DLNARenderer_upnpPrevious, args => 0},
  257. seek => {method => \&DLNARenderer_seek, args => 1},
  258. multiRoomVolume => {method => \&DLNARenderer_setMultiRoomVolume, args => 1, argdef => "slider,0,1,100", caskeid => 1},
  259. stereo => {method => \&DLNARenderer_setStereoMode, args => 3, caskeid => 1},
  260. standalone => {method => \&DLNARenderer_setStandaloneMode, args => 0, caskeid => 1},
  261. playEverywhere => {method => \&DLNARenderer_playEverywhere, args => 0, caskeid => 1},
  262. stopPlayEverywhere => {method => \&DLNARenderer_stopPlayEverywhere, args => 0, caskeid => 1},
  263. addUnit => {method => \&DLNARenderer_addUnit, args => 1, argdef => $hash->{helper}{caskeidClients}, caskeid => 1},
  264. removeUnit => {method => \&DLNARenderer_removeUnit, args => 1, argdef => ReadingsVal($hash->{NAME}, "multiRoomUnits", ""), caskeid => 1},
  265. saveGroupAs => {method => \&DLNARenderer_saveGroupAs, args => 1, caskeid => 1},
  266. enableBTCaskeid => {method => \&DLNARenderer_enableBTCaskeid, args => 0, caskeid => 1},
  267. disableBTCaskeid => {method => \&DLNARenderer_disableBTCaskeid, args => 0, caskeid => 1},
  268. off => {method => \&DLNARenderer_upnpStop, args => 0},
  269. stop => {method => \&DLNARenderer_upnpStop, args => 0},
  270. loadGroup => {method => \&DLNARenderer_loadGroup, args => 1, caskeid => 1},
  271. on => {method => \&DLNARenderer_on, args => 0},
  272. stream => {method => \&DLNARenderer_stream, args => 1},
  273. channel => {method => \&DLNARenderer_channel, args => 1, argdef => "1,2,3,4,5,6,7,8,9,10"},
  274. speak => {method => \&DLNARenderer_speak, args => 1}
  275. };
  276. if($set_method_mapping->{$ctrlParam}) {
  277. if($set_method_mapping->{$ctrlParam}{args} != int(@params)) {
  278. return "DLNARenderer: $ctrlParam requires $set_method_mapping->{$ctrlParam}{args} parameter.";
  279. }
  280. #params array till args number
  281. my @args = @params[0 .. $set_method_mapping->{$ctrlParam}{args}];
  282. $set_method_mapping->{$ctrlParam}{method}->($hash, @args);
  283. } else {
  284. my $cmdList;
  285. foreach my $cmd (keys %$set_method_mapping) {
  286. next if($hash->{helper}{caskeid} == 0 && $set_method_mapping->{$cmd}{caskeid} && $set_method_mapping->{$cmd}{caskeid} == 1);
  287. if($set_method_mapping->{$cmd}{args} == 0) {
  288. $cmdList .= $cmd.":noArg ";
  289. } else {
  290. if($set_method_mapping->{$cmd}{argdef}) {
  291. $cmdList .= $cmd.":".$set_method_mapping->{$cmd}{argdef}." ";
  292. } else {
  293. $cmdList .= $cmd." ";
  294. }
  295. }
  296. }
  297. return SetExtensions($hash, $cmdList, $name, $ctrlParam, @params);
  298. }
  299. return undef;
  300. }
  301. ##############################
  302. ##### SET FUNCTIONS ##########
  303. ##############################
  304. sub DLNARenderer_speak {
  305. my ($hash, $ttsText) = @_;
  306. my $ttsLang = AttrVal($hash->{NAME}, "ttsLanguage", "en");
  307. return "DLNARenderer: Maximum text length is 100 characters." if(length($ttsText) > 100);
  308. DLNARenderer_stream($hash, "http://translate.google.com/translate_tts?tl=$ttsLang&client=tw-ob&q=$ttsText", "");
  309. }
  310. sub DLNARenderer_channel {
  311. my ($hash, $channelNr) = @_;
  312. my $stream = AttrVal($hash->{NAME}, sprintf("channel_%02d", $channelNr), "");
  313. if($stream eq "") {
  314. return "DLNARenderer: Set channel_XX attribute first.";
  315. }
  316. my $meta = "";
  317. if (substr($stream,0,10) eq "<DIDL-Lite") {
  318. eval {
  319. my $xml = XMLin($stream);
  320. $meta = $stream;
  321. $stream = $xml->{"item"}{"res"}{"content"};
  322. };
  323. if($@) {
  324. Log3 $hash, 2, "DLNARenderer: Incorrect DIDL-Lite format, $@";
  325. }
  326. }
  327. DLNARenderer_stream($hash, $stream, $meta);
  328. readingsSingleUpdate($hash, "channel", $channelNr, 1);
  329. }
  330. sub DLNARenderer_stream {
  331. my ($hash, $stream, $meta) = @_;
  332. if (!defined($meta)) {
  333. DLNARenderer_generateDidlLiteAndPlay($hash, $stream);
  334. return undef;
  335. }
  336. DLNARenderer_upnpSetAVTransportURI($hash, $stream, $meta);
  337. DLNARenderer_play($hash);
  338. readingsSingleUpdate($hash, "stream", $stream, 1);
  339. }
  340. sub DLNARenderer_on {
  341. my ($hash) = @_;
  342. if (defined($hash->{READINGS}{stream})) {
  343. my $lastStream = $hash->{READINGS}{stream}{VAL};
  344. if ($lastStream) {
  345. DLNARenderer_upnpSetAVTransportURI($hash, $lastStream);
  346. DLNARenderer_play($hash);
  347. }
  348. }
  349. }
  350. sub DLNARenderer_convertVolumeToAbsolute {
  351. my ($hash, $targetVolume) = @_;
  352. if(substr($targetVolume, 0, 1) eq "+" or
  353. substr($targetVolume, 0, 1) eq "-") {
  354. $targetVolume = ReadingsVal($hash->{NAME}, "volume", 0) + $targetVolume;
  355. }
  356. return $targetVolume;
  357. }
  358. sub DLNARenderer_volume {
  359. my ($hash, $targetVolume) = @_;
  360. $targetVolume = DLNARenderer_convertVolumeToAbsolute($hash, $targetVolume);
  361. DLNARenderer_upnpSetVolume($hash, $targetVolume);
  362. }
  363. sub DLNARenderer_mute {
  364. my ($hash, $muteState) = @_;
  365. if($muteState eq "on") {
  366. $muteState = 1;
  367. } else {
  368. $muteState = 0;
  369. }
  370. DLNARenderer_upnpSetMute($hash, $muteState);
  371. }
  372. sub DLNARenderer_removeUnit {
  373. my ($hash, $unitToRemove) = @_;
  374. DLNARenderer_removeUnitToPlay($hash, $unitToRemove);
  375. my $multiRoomUnitsReading = "";
  376. my @multiRoomUnits = split(",", ReadingsVal($hash->{NAME}, "multiRoomUnits", ""));
  377. foreach my $unit (@multiRoomUnits) {
  378. $multiRoomUnitsReading .= ",".$unit if($unit ne $unitToRemove);
  379. }
  380. $multiRoomUnitsReading = substr($multiRoomUnitsReading, 1) if($multiRoomUnitsReading ne "");
  381. readingsSingleUpdate($hash, "multiRoomUnits", $multiRoomUnitsReading, 1);
  382. return undef;
  383. }
  384. sub DLNARenderer_loadGroup {
  385. my ($hash, $groupName) = @_;
  386. my $groupMembers = DLNARenderer_getGroupDefinition($hash, $groupName);
  387. return "DLNARenderer: Group $groupName not defined." if(!defined($groupMembers));
  388. DLNARenderer_destroyCurrentSession($hash);
  389. my $leftSpeaker = "";
  390. my $rightSpeaker = "";
  391. my @groupMembersArray = split(",", $groupMembers);
  392. foreach my $member (@groupMembersArray) {
  393. if($member =~ /^R:([a-zA-Z0-9äöüßÄÜÖ_]+)/) {
  394. $rightSpeaker = $1;
  395. } elsif($member =~ /^L:([a-zA-Z0-9äöüßÄÜÖ_]+)/) {
  396. $leftSpeaker = $1;
  397. } else {
  398. DLNARenderer_addUnit($hash, $member);
  399. }
  400. }
  401. if($leftSpeaker ne "" && $rightSpeaker ne "") {
  402. DLNARenderer_setStereoMode($hash, $leftSpeaker, $rightSpeaker, $groupName);
  403. }
  404. }
  405. sub DLNARenderer_stopPlayEverywhere {
  406. my ($hash) = @_;
  407. DLNARenderer_destroyCurrentSession($hash);
  408. readingsSingleUpdate($hash, "multiRoomUnits", "", 1);
  409. return undef;
  410. }
  411. sub DLNARenderer_playEverywhere {
  412. my ($hash) = @_;
  413. my $multiRoomUnits = "";
  414. my @caskeidClients = DLNARenderer_getAllDLNARenderersWithCaskeid($hash);
  415. foreach my $client (@caskeidClients) {
  416. if($client->{UDN} ne $hash->{UDN}) {
  417. DLNARenderer_addUnitToPlay($hash, substr($client->{UDN},5));
  418. my $multiRoomUnits = ReadingsVal($hash->{NAME}, "multiRoomUnits", "");
  419. $multiRoomUnits .= "," if($multiRoomUnits ne "");
  420. $multiRoomUnits .= ReadingsVal($client->{NAME}, "friendlyName", "");
  421. readingsSingleUpdate($hash, "multiRoomUnits", $multiRoomUnits, 1);
  422. }
  423. }
  424. return undef;
  425. }
  426. sub DLNARenderer_setMultiRoomVolume {
  427. my ($hash, $targetVolume) = @_;
  428. #change volume of this device
  429. DLNARenderer_volume($hash, $targetVolume);
  430. #handle volume for all devices in the current group
  431. #iterate through group and change volume relative to the current volume of this device
  432. my $mainVolumeDiff = DLNARenderer_convertVolumeToAbsolute($hash, $targetVolume) - ReadingsVal($hash->{NAME}, "volume", 0);
  433. my $multiRoomUnits = ReadingsVal($hash->{NAME}, "multiRoomUnits", "");
  434. my @multiRoomUnitsArray = split(",", $multiRoomUnits);
  435. foreach my $unit (@multiRoomUnitsArray) {
  436. my $devHash = DLNARenderer_getHashByFriendlyName($hash, $unit);
  437. my $newVolume = ReadingsVal($devHash->{NAME}, "volume", 0) + $mainVolumeDiff;
  438. if($newVolume > 100) {
  439. $newVolume = 100;
  440. } elsif($newVolume < 0) {
  441. $newVolume = 0;
  442. }
  443. DLNARenderer_volume($devHash, $newVolume);
  444. }
  445. return undef;
  446. }
  447. sub DLNARenderer_pauseToggle {
  448. my ($hash) = @_;
  449. if($hash->{READINGS}{state}{VAL} eq "paused") {
  450. DLNARenderer_play($hash);
  451. } else {
  452. DLNARenderer_upnpPause($hash);
  453. }
  454. }
  455. sub DLNARenderer_play {
  456. my ($hash) = @_;
  457. #start play
  458. if($hash->{helper}{caskeid}) {
  459. if($hash->{READINGS}{sessionId}{VAL} eq "") {
  460. DLNARenderer_createSession($hash);
  461. }
  462. DLNARenderer_upnpSyncPlay($hash);
  463. } else {
  464. DLNARenderer_upnpPlay($hash);
  465. }
  466. return undef;
  467. }
  468. ###########################
  469. ##### CASKEID #############
  470. ###########################
  471. # BTCaskeid
  472. sub DLNARenderer_enableBTCaskeid {
  473. my ($hash) = @_;
  474. DLNARenderer_upnpAddToGroup($hash, "4DAA44C0-8291-11E3-BAA7-0800200C9A66", "Bluetooth");
  475. }
  476. sub DLNARenderer_disableBTCaskeid {
  477. my ($hash) = @_;
  478. DLNARenderer_upnpRemoveFromGroup($hash, "4DAA44C0-8291-11E3-BAA7-0800200C9A66");
  479. }
  480. # Stereo Mode
  481. sub DLNARenderer_setStereoMode {
  482. my ($hash, $leftSpeaker, $rightSpeaker, $name) = @_;
  483. DLNARenderer_destroyCurrentSession($hash);
  484. my @multiRoomDevices = DLNARenderer_getAllDLNARenderersWithCaskeid($hash);
  485. my $uuid = DLNARenderer_createUuid($hash);
  486. foreach my $device (@multiRoomDevices) {
  487. if(ReadingsVal($device->{NAME}, "friendlyName", "") eq $leftSpeaker) {
  488. DLNARenderer_setMultiChannelSpeaker($device, "left", $uuid, $leftSpeaker);
  489. readingsSingleUpdate($hash, "stereoLeft", $leftSpeaker, 1);
  490. } elsif(ReadingsVal($device->{NAME}, "friendlyName", "") eq $rightSpeaker) {
  491. DLNARenderer_setMultiChannelSpeaker($device, "right", $uuid, $rightSpeaker);
  492. readingsSingleUpdate($hash, "stereoRight", $rightSpeaker, 1);
  493. }
  494. }
  495. }
  496. sub DLNARenderer_updateStereoMode {
  497. my ($hash) = @_;
  498. if(!defined($hash->{helper}{device})) {
  499. InternalTimer(gettimeofday() + 10, 'DLNARenderer_updateStereoMode', $hash, 0);
  500. return undef;
  501. }
  502. if($hash->{helper}{caskeid} == 0) {
  503. return undef;
  504. }
  505. my $result = DLNARenderer_upnpGetMultiChannelSpeaker($hash);
  506. if($result) {
  507. InternalTimer(gettimeofday() + 300, 'DLNARenderer_updateStereoMode', $hash, 0);
  508. DLNARenderer_readingsSingleUpdateIfChanged($hash, "stereoSupport", 1, 1);
  509. } else {
  510. #speaker does not support multi channel
  511. DLNARenderer_readingsSingleUpdateIfChanged($hash, "stereoSupport", 0, 1);
  512. return undef;
  513. }
  514. my $mcsType = $result->getValue("CurrentMCSType");
  515. my $mcsId = $result->getValue("CurrentMCSID");
  516. my $mcsFriendlyName = $result->getValue("CurrentMCSFriendlyName");
  517. my $mcsSpeakerChannel = $result->getValue("CurrentSpeakerChannel");
  518. DLNARenderer_readingsSingleUpdateIfChanged($hash, "stereoPairName", $mcsFriendlyName, 1);
  519. DLNARenderer_readingsSingleUpdateIfChanged($hash, "stereoId", $mcsId, 1);
  520. if($mcsId eq "") {
  521. DLNARenderer_readingsSingleUpdateIfChanged($hash, "stereoLeft", "", 1);
  522. DLNARenderer_readingsSingleUpdateIfChanged($hash, "stereoRight", "", 1);
  523. } else {
  524. #THIS speaker is the left or right speaker
  525. DLNARenderer_setStereoSpeakerReading($hash, $hash, $mcsType, $mcsId, $mcsFriendlyName, $mcsSpeakerChannel);
  526. #set left/right speaker for OTHER speaker if OTHER speaker has same mcsId
  527. my @allHashes = DLNARenderer_getAllDLNARenderersWithCaskeid($hash);
  528. foreach my $hash2 (@allHashes) {
  529. my $result2 = DLNARenderer_upnpGetMultiChannelSpeaker($hash2);
  530. next if(!defined($result2));
  531. my $mcsType2 = $result2->getValue("CurrentMCSType");
  532. my $mcsId2 = $result2->getValue("CurrentMCSID");
  533. my $mcsFriendlyName2 = $result2->getValue("CurrentMCSFriendlyName");
  534. my $mcsSpeakerChannel2 = $result2->getValue("CurrentSpeakerChannel");
  535. if($mcsId2 eq $mcsId) {
  536. DLNARenderer_setStereoSpeakerReading($hash, $hash2, $mcsType2, $mcsId2, $mcsFriendlyName2, $mcsSpeakerChannel2);
  537. }
  538. }
  539. }
  540. }
  541. sub DLNARenderer_setStereoSpeakerReading {
  542. my ($hash, $speakerHash, $mcsType, $mcsId, $mcsFriendlyName, $mcsSpeakerChannel) = @_;
  543. DLNARenderer_readingsSingleUpdateIfChanged($hash, "stereoId", $mcsId, 1);
  544. if($mcsSpeakerChannel eq "LEFT_FRONT") {
  545. DLNARenderer_readingsSingleUpdateIfChanged($hash, "stereoLeft", ReadingsVal($speakerHash->{NAME}, "friendlyName", ""), 1);
  546. } elsif($mcsSpeakerChannel eq "RIGHT_FRONT") {
  547. DLNARenderer_readingsSingleUpdateIfChanged($hash, "stereoRight", ReadingsVal($speakerHash->{NAME}, "friendlyName", ""), 1);
  548. }
  549. }
  550. sub DLNARenderer_readingsSingleUpdateIfChanged {
  551. my ($hash, $reading, $value, $trigger) = @_;
  552. my $curVal = ReadingsVal($hash->{NAME}, $reading, "");
  553. if($curVal ne $value) {
  554. readingsSingleUpdate($hash, $reading, $value, $trigger);
  555. }
  556. }
  557. sub DLNARenderer_setMultiChannelSpeaker {
  558. my ($hash, $mode, $uuid, $name) = @_;
  559. my $uuidStr;
  560. if($mode eq "standalone") {
  561. DLNARenderer_upnpSetMultiChannelSpeaker($hash, "STANDALONE", "", "", "STANDALONE_SPEAKER");
  562. } elsif($mode eq "left") {
  563. DLNARenderer_upnpSetMultiChannelSpeaker($hash, "STEREO", $uuid, $name, "LEFT_FRONT");
  564. } elsif($mode eq "right") {
  565. DLNARenderer_upnpSetMultiChannelSpeaker($hash, "STEREO", $uuid, $name, "RIGHT_FRONT");
  566. }
  567. return undef;
  568. }
  569. sub DLNARenderer_setStandaloneMode {
  570. my ($hash) = @_;
  571. my @multiRoomDevices = DLNARenderer_getAllDLNARenderersWithCaskeid($hash);
  572. my $rightSpeaker = ReadingsVal($hash->{NAME}, "stereoRight", "");
  573. my $leftSpeaker = ReadingsVal($hash->{NAME}, "stereoLeft", "");
  574. foreach my $device (@multiRoomDevices) {
  575. if(ReadingsVal($device->{NAME}, "friendlyName", "") eq $leftSpeaker or
  576. ReadingsVal($device->{NAME}, "friendlyName", "") eq $rightSpeaker) {
  577. DLNARenderer_setMultiChannelSpeaker($device, "standalone", "", "");
  578. }
  579. }
  580. readingsSingleUpdate($hash, "stereoLeft", "", 1);
  581. readingsSingleUpdate($hash, "stereoRight", "", 1);
  582. readingsSingleUpdate($hash, "stereoId", "", 1);
  583. return undef;
  584. }
  585. sub DLNARenderer_createUuid {
  586. my ($hash) = @_;
  587. my $ug = Data::UUID->new();
  588. my $uuid = $ug->create();
  589. my $uuidStr = $ug->to_string($uuid);
  590. return $uuidStr;
  591. }
  592. # SessionManagement
  593. sub DLNARenderer_createSession {
  594. my ($hash) = @_;
  595. return DLNARenderer_upnpCreateSession($hash, "FHEM_Session");
  596. }
  597. sub DLNARenderer_getSession {
  598. my ($hash) = @_;
  599. return DLNARenderer_upnpGetSession($hash);
  600. }
  601. sub DLNARenderer_destroySession {
  602. my ($hash, $session) = @_;
  603. return DLNARenderer_upnpDestroySession($hash, $session);
  604. }
  605. sub DLNARenderer_destroyCurrentSession {
  606. my ($hash) = @_;
  607. my $result = DLNARenderer_getSession($hash);
  608. if($result->getValue("SessionID") ne "") {
  609. DLNARenderer_destroySession($hash, $result->getValue("SessionID"));
  610. }
  611. }
  612. sub DLNARenderer_addUnitToPlay {
  613. my ($hash, $unit) = @_;
  614. my $session = DLNARenderer_getSession($hash)->getValue("SessionID");
  615. if($session eq "") {
  616. $session = DLNARenderer_createSession($hash)->getValue("SessionID");
  617. }
  618. DLNARenderer_addUnitToSession($hash, $unit, $session);
  619. }
  620. sub DLNARenderer_removeUnitToPlay {
  621. my ($hash, $unit) = @_;
  622. my $session = DLNARenderer_getSession($hash)->getValue("SessionID");
  623. if($session ne "") {
  624. DLNARenderer_removeUnitFromSession($hash, $unit, $session);
  625. }
  626. }
  627. sub DLNARenderer_addUnitToSession {
  628. my ($hash, $uuid, $session) = @_;
  629. return DLNARenderer_upnpAddUnitToSession($hash, $session, $uuid);
  630. }
  631. sub DLNARenderer_removeUnitFromSession {
  632. my ($hash, $uuid, $session) = @_;
  633. return DLNARenderer_upnpRemoveUnitFromSession($hash, $session, $uuid);
  634. }
  635. # Group Definitions
  636. sub DLNARenderer_getGroupDefinition {
  637. #used for ... play Bad ...
  638. my ($hash, $groupName) = @_;
  639. my $currentGroupSettings = AttrVal($hash->{NAME}, "multiRoomGroups", "");
  640. #regex Bad[MUNET1,MUNET2],WZ[L:MUNET2,R:MUNET3],...
  641. while ($currentGroupSettings =~ /([a-zA-Z0-9äöüßÄÜÖ_]+)\[([a-zA-Z,0-9:äöüßÄÜÖ_]+)/g) {
  642. my $group = $1;
  643. my $groupMembers = $2;
  644. Log3 $hash, 4, "DLNARenderer: Groupdefinition $group => $groupMembers";
  645. if($group eq $groupName) {
  646. return $groupMembers;
  647. }
  648. }
  649. return undef;
  650. }
  651. sub DLNARenderer_saveGroupAs {
  652. my ($hash, $groupName) = @_;
  653. my $currentGroupSettings = AttrVal($hash->{NAME}, "multiRoomGroups", "");
  654. $currentGroupSettings .= "," if($currentGroupSettings ne "");
  655. #session details
  656. my $currentSession = ReadingsVal($hash->{NAME}, "multiRoomUnits", "");
  657. #stereo mode
  658. my $stereoLeft = ReadingsVal($hash->{NAME}, "stereoLeft", "");
  659. my $stereoRight = ReadingsVal($hash->{NAME}, "stereoRight", "");
  660. my $stereoDevices = "L:$stereoLeft,R:$stereoRight" if($stereoLeft ne "" && $stereoRight ne "");
  661. return undef if($currentSession eq "" && $stereoLeft eq "" && $stereoRight eq "");
  662. $stereoDevices .= "," if($currentSession ne "" && $stereoDevices ne "");
  663. my $groupDefinition = $currentGroupSettings.$groupName."[".$stereoDevices.$currentSession."]";
  664. #save current session as group
  665. CommandAttr(undef, "$hash->{NAME} multiRoomGroups $groupDefinition");
  666. return undef;
  667. }
  668. sub DLNARenderer_addUnit {
  669. my ($hash, $unitName) = @_;
  670. my @caskeidClients = DLNARenderer_getAllDLNARenderersWithCaskeid($hash);
  671. foreach my $client (@caskeidClients) {
  672. if(ReadingsVal($client->{NAME}, "friendlyName", "") eq $unitName) {
  673. my @multiRoomUnits = split(",", ReadingsVal($hash->{NAME}, "multiRoomUnits", ""));
  674. foreach my $unit (@multiRoomUnits) {
  675. #skip if unit is already part of the session
  676. return undef if($unit eq $unitName);
  677. }
  678. #add unit to session
  679. DLNARenderer_addUnitToPlay($hash, substr($client->{UDN},5));
  680. return undef;
  681. }
  682. }
  683. return "DLNARenderer: No unit $unitName found.";
  684. }
  685. ##############################
  686. ####### UPNP FUNCTIONS #######
  687. ##############################
  688. sub DLNARenderer_upnpPause {
  689. my ($hash) = @_;
  690. return DLNARenderer_upnpCallAVTransport($hash, "Pause", 0);
  691. }
  692. sub DLNARenderer_upnpSetAVTransportURI {
  693. my ($hash, $stream, $meta) = @_;
  694. if (!defined($meta)) { $meta = ""; }
  695. return DLNARenderer_upnpCallAVTransport($hash, "SetAVTransportURI", 0, $stream, $meta);
  696. }
  697. sub DLNARenderer_upnpStop {
  698. my ($hash) = @_;
  699. return DLNARenderer_upnpCallAVTransport($hash, "Stop", 0);
  700. }
  701. sub DLNARenderer_upnpSeek {
  702. my ($hash, $seekTime) = @_;
  703. return DLNARenderer_upnpCallAVTransport($hash, "Seek", 0, "REL_TIME", $seekTime);
  704. }
  705. sub DLNARenderer_upnpNext {
  706. my ($hash) = @_;
  707. return DLNARenderer_upnpCallAVTransport($hash, "Next", 0);
  708. }
  709. sub DLNARenderer_upnpPrevious {
  710. my ($hash) = @_;
  711. return DLNARenderer_upnpCallAVTransport($hash, "Previous", 0);
  712. }
  713. sub DLNARenderer_upnpPlay {
  714. my ($hash) = @_;
  715. return DLNARenderer_upnpCallAVTransport($hash, "Play", 0, 1);
  716. }
  717. sub DLNARenderer_upnpSyncPlay {
  718. my ($hash) = @_;
  719. return DLNARenderer_upnpCallAVTransport($hash, "SyncPlay", 0, 1, "REL_TIME", "", "", "", "PUREDEVICECLOCK1");
  720. }
  721. sub DLNARenderer_upnpCallAVTransport {
  722. my ($hash, $method, @args) = @_;
  723. return DLNARenderer_upnpCall($hash, 'AVTransport', $method, @args);
  724. }
  725. sub DLNARenderer_upnpGetMultiChannelSpeaker {
  726. my ($hash) = @_;
  727. return DLNARenderer_upnpCallSpeakerManagement($hash, "GetMultiChannelSpeaker");
  728. }
  729. sub DLNARenderer_upnpSetMultiChannelSpeaker {
  730. my ($hash, @args) = @_;
  731. return DLNARenderer_upnpCallSpeakerManagement($hash, "SetMultiChannelSpeaker", @args);
  732. }
  733. sub DLNARenderer_upnpCallSpeakerManagement {
  734. my ($hash, $method, @args) = @_;
  735. return DLNARenderer_upnpCall($hash, 'SpeakerManagement', $method, @args);
  736. }
  737. sub DLNARenderer_upnpAddUnitToSession {
  738. my ($hash, $session, $uuid) = @_;
  739. return DLNARenderer_upnpCallSessionManagement($hash, "AddUnitToSession", $session, $uuid);
  740. }
  741. sub DLNARenderer_upnpRemoveUnitFromSession {
  742. my ($hash, $session, $uuid) = @_;
  743. return DLNARenderer_upnpCallSessionManagement($hash, "RemoveUnitFromSession", $session, $uuid);
  744. }
  745. sub DLNARenderer_upnpDestroySession {
  746. my ($hash, $session) = @_;
  747. return DLNARenderer_upnpCallSessionManagement($hash, "DestroySession", $session);
  748. }
  749. sub DLNARenderer_upnpCreateSession {
  750. my ($hash, $name) = @_;
  751. return DLNARenderer_upnpCallSessionManagement($hash, "CreateSession", $name);
  752. }
  753. sub DLNARenderer_upnpGetSession {
  754. my ($hash) = @_;
  755. return DLNARenderer_upnpCallSessionManagement($hash, "GetSession");
  756. }
  757. sub DLNARenderer_upnpAddToGroup {
  758. my ($hash, $unit, $name) = @_;
  759. return DLNARenderer_upnpCallSpeakerManagement($hash, "AddToGroup", $unit, $name, "");
  760. }
  761. sub DLNARenderer_upnpRemoveFromGroup {
  762. my ($hash, $unit) = @_;
  763. return DLNARenderer_upnpCallSpeakerManagement($hash, "RemoveFromGroup", $unit);
  764. }
  765. sub DLNARenderer_upnpCallSessionManagement {
  766. my ($hash, $method, @args) = @_;
  767. return DLNARenderer_upnpCall($hash, 'SessionManagement', $method, @args);
  768. }
  769. sub DLNARenderer_upnpSetVolume {
  770. my ($hash, $targetVolume) = @_;
  771. return DLNARenderer_upnpCallRenderingControl($hash, "SetVolume", 0, "Master", $targetVolume);
  772. }
  773. sub DLNARenderer_upnpSetMute {
  774. my ($hash, $muteState) = @_;
  775. return DLNARenderer_upnpCallRenderingControl($hash, "SetMute", 0, "Master", $muteState);
  776. }
  777. sub DLNARenderer_upnpCallRenderingControl {
  778. my ($hash, $method, @args) = @_;
  779. return DLNARenderer_upnpCall($hash, 'RenderingControl', $method, @args);
  780. }
  781. sub DLNARenderer_upnpCall {
  782. my ($hash, $service, $method, @args) = @_;
  783. my $upnpService = DLNARenderer_upnpGetService($hash, $service);
  784. my $ret = undef;
  785. my $i = 0;
  786. do {
  787. eval {
  788. my $upnpServiceCtrlProxy = $upnpService->controlProxy();
  789. my $methodExists = $upnpService->getAction($method);
  790. if($methodExists) {
  791. $ret = $upnpServiceCtrlProxy->$method(@args);
  792. Log3 $hash, 5, "DLNARenderer: $service, $method(".join(",",@args).") succeed.";
  793. } else {
  794. Log3 $hash, 4, "DLNARenderer: $service, $method(".join(",",@args).") does not exist.";
  795. }
  796. };
  797. if($@) {
  798. Log3 $hash, 3, "DLNARenderer: $service, $method(".join(",",@args).") failed, $@";
  799. }
  800. $i = $i+1;
  801. } while(!defined($ret) && $i < 3);
  802. return $ret;
  803. }
  804. sub DLNARenderer_upnpGetService {
  805. my ($hash, $service) = @_;
  806. my $upnpService;
  807. foreach my $srvc ($hash->{helper}{device}->services) {
  808. my @srvcParts = split(":", $srvc->serviceType);
  809. my $serviceName = $srvcParts[-2];
  810. if($serviceName eq $service) {
  811. Log3 $hash, 5, "DLNARenderer: $service: ".$srvc->serviceType." found. OK.";
  812. $upnpService = $srvc;
  813. }
  814. }
  815. if(!defined($upnpService)) {
  816. Log3 $hash, 4, "DLNARenderer: $service unknown for $hash->{NAME}.";
  817. return undef;
  818. }
  819. return $upnpService;
  820. }
  821. ##############################
  822. ####### EVENT HANDLING #######
  823. ##############################
  824. sub DLNARenderer_processEventXml {
  825. my ($hash, $property, $xml) = @_;
  826. Log3 $hash, 4, "DLNARenderer: ".Dumper($xml);
  827. if($property eq "LastChange") {
  828. return undef if($xml eq "");
  829. if($xml->{Event}) {
  830. if (index($xml->{Event}{xmlns},"urn:schemas-upnp-org:metadata-1-0/AVT")==0) {
  831. #process AV Transport
  832. my $e = $xml->{Event}{InstanceID};
  833. #DLNARenderer_updateReadingByEvent($hash, "NumberOfTracks", $e->{NumberOfTracks});
  834. DLNARenderer_updateReadingByEvent($hash, "transportState", $e->{TransportState});
  835. DLNARenderer_updateReadingByEvent($hash, "transportStatus", $e->{TransportStatus});
  836. #DLNARenderer_updateReadingByEvent($hash, "TransportPlaySpeed", $e->{TransportPlaySpeed});
  837. #DLNARenderer_updateReadingByEvent($hash, "PlaybackStorageMedium", $e->{PlaybackStorageMedium});
  838. #DLNARenderer_updateReadingByEvent($hash, "RecordStorageMedium", $e->{RecordStorageMedium});
  839. #DLNARenderer_updateReadingByEvent($hash, "RecordMediumWriteStatus", $e->{RecordMediumWriteStatus});
  840. #DLNARenderer_updateReadingByEvent($hash, "CurrentRecordQualityMode", $e->{CurrentRecordQualityMode});
  841. #DLNARenderer_updateReadingByEvent($hash, "PossibleRecordQualityMode", $e->{PossibleRecordQualityMode});
  842. DLNARenderer_updateReadingByEvent($hash, "currentTrackURI", $e->{CurrentTrackURI});
  843. #DLNARenderer_updateReadingByEvent($hash, "AVTransportURI", $e->{AVTransportURI});
  844. DLNARenderer_updateReadingByEvent($hash, "nextAVTransportURI", $e->{NextAVTransportURI});
  845. #DLNARenderer_updateReadingByEvent($hash, "RelativeTimePosition", $e->{RelativeTimePosition});
  846. #DLNARenderer_updateReadingByEvent($hash, "AbsoluteTimePosition", $e->{AbsoluteTimePosition});
  847. #DLNARenderer_updateReadingByEvent($hash, "RelativeCounterPosition", $e->{RelativeCounterPosition});
  848. #DLNARenderer_updateReadingByEvent($hash, "AbsoluteCounterPosition", $e->{AbsoluteCounterPosition});
  849. #DLNARenderer_updateReadingByEvent($hash, "CurrentTrack", $e->{CurrentTrack});
  850. #DLNARenderer_updateReadingByEvent($hash, "CurrentMediaDuration", $e->{CurrentMediaDuration});
  851. #DLNARenderer_updateReadingByEvent($hash, "CurrentTrackDuration", $e->{CurrentTrackDuration});
  852. #DLNARenderer_updateReadingByEvent($hash, "CurrentPlayMode", $e->{CurrentPlayMode});
  853. #handle metadata
  854. #DLNARenderer_updateReadingByEvent($hash, "AVTransportURIMetaData", $e->{AVTransportURIMetaData});
  855. #DLNARenderer_updateMetaData($hash, "current", $e->{AVTransportURIMetaData});
  856. #DLNARenderer_updateReadingByEvent($hash, "NextAVTransportURIMetaData", $e->{NextAVTransportURIMetaData});
  857. DLNARenderer_updateMetaData($hash, "next", $e->{NextAVTransportURIMetaData});
  858. #use only CurrentTrackMetaData instead of AVTransportURIMetaData
  859. #DLNARenderer_updateReadingByEvent($hash, "CurrentTrackMetaData", $e->{CurrentTrackMetaData});
  860. DLNARenderer_updateMetaData($hash, "current", $e->{CurrentTrackMetaData});
  861. #update state
  862. my $transportState = ReadingsVal($hash->{NAME}, "transportState", "");
  863. if(ReadingsVal($hash->{NAME}, "presence", "") ne "offline") {
  864. if($transportState eq "PAUSED_PLAYBACK") {
  865. readingsSingleUpdate($hash, "state", "paused", 1);
  866. } elsif($transportState eq "PLAYING") {
  867. readingsSingleUpdate($hash, "state", "playing", 1);
  868. } elsif($transportState eq "TRANSITIONING") {
  869. readingsSingleUpdate($hash, "state", "buffering", 1);
  870. } elsif($transportState eq "STOPPED") {
  871. readingsSingleUpdate($hash, "state", "stopped", 1);
  872. } elsif($transportState eq "NO_MEDIA_PRESENT") {
  873. readingsSingleUpdate($hash, "state", "online", 1);
  874. }
  875. }
  876. } elsif (index($xml->{Event}{xmlns},"urn:schemas-upnp-org:metadata-1-0/RCS")==0) {
  877. #process RenderingControl
  878. my $e = $xml->{Event}{InstanceID};
  879. DLNARenderer_updateVolumeByEvent($hash, "mute", $e->{Mute});
  880. DLNARenderer_updateVolumeByEvent($hash, "volume", $e->{Volume});
  881. readingsSingleUpdate($hash, "multiRoomVolume", ReadingsVal($hash->{NAME}, "volume", 0), 1);
  882. } elsif ($xml->{Event}{xmlns} eq "FIXME SpeakerManagement") {
  883. #process SpeakerManagement
  884. }
  885. }
  886. } elsif($property eq "Groups") {
  887. #handle BTCaskeid
  888. my $btCaskeidState = 0;
  889. foreach my $group (@{$xml->{groups}{group}}) {
  890. #"4DAA44C0-8291-11E3-BAA7-0800200C9A66", "Bluetooth"
  891. if($group->{id} eq "4DAA44C0-8291-11E3-BAA7-0800200C9A66") {
  892. $btCaskeidState = 1;
  893. }
  894. }
  895. #TODO update only if changed
  896. readingsSingleUpdate($hash, "btCaskeid", $btCaskeidState, 1);
  897. } elsif($property eq "SessionID") {
  898. #TODO search for other speakers with same sessionId and add them to multiRoomUnits
  899. readingsSingleUpdate($hash, "sessionId", $xml, 1);
  900. }
  901. return undef;
  902. }
  903. sub DLNARenderer_updateReadingByEvent {
  904. my ($hash, $readingName, $xmlEvent) = @_;
  905. my $currVal = ReadingsVal($hash->{NAME}, $readingName, "");
  906. if($xmlEvent) {
  907. Log3 $hash, 4, "DLNARenderer: Update reading $readingName with ".$xmlEvent->{val};
  908. my $val = $xmlEvent->{val};
  909. $val = "" if(ref $val eq ref {});
  910. if($val ne $currVal) {
  911. readingsSingleUpdate($hash, $readingName, $val, 1);
  912. }
  913. }
  914. return undef;
  915. }
  916. sub DLNARenderer_updateVolumeByEvent {
  917. my ($hash, $readingName, $volume) = @_;
  918. my $balance = 0;
  919. my $balanceSupport = 0;
  920. foreach my $vol (@{$volume}) {
  921. my $channel = $vol->{Channel} ? $vol->{Channel} : $vol->{channel};
  922. if($channel) {
  923. if($channel eq "Master") {
  924. DLNARenderer_updateReadingByEvent($hash, $readingName, $vol);
  925. } elsif($channel eq "LF") {
  926. $balance -= $vol->{val};
  927. $balanceSupport = 1;
  928. } elsif($channel eq "RF") {
  929. $balance += $vol->{val};
  930. $balanceSupport = 1;
  931. }
  932. } else {
  933. DLNARenderer_updateReadingByEvent($hash, $readingName, $vol);
  934. }
  935. }
  936. if($readingName eq "volume" && $balanceSupport == 1) {
  937. readingsSingleUpdate($hash, "balance", $balance, 1);
  938. }
  939. return undef;
  940. }
  941. sub DLNARenderer_updateMetaData {
  942. my ($hash, $prefix, $metaData) = @_;
  943. my $metaDataAvailable = 0;
  944. $metaDataAvailable = 1 if(defined($metaData) && $metaData->{val} && $metaData->{val} ne "");
  945. if($metaDataAvailable) {
  946. my $xml;
  947. if($metaData->{val} eq "NOT_IMPLEMENTED") {
  948. readingsSingleUpdate($hash, $prefix."Title", "", 1);
  949. readingsSingleUpdate($hash, $prefix."Artist", "", 1);
  950. readingsSingleUpdate($hash, $prefix."Album", "", 1);
  951. readingsSingleUpdate($hash, $prefix."AlbumArtist", "", 1);
  952. readingsSingleUpdate($hash, $prefix."AlbumArtURI", "", 1);
  953. readingsSingleUpdate($hash, $prefix."OriginalTrackNumber", "", 1);
  954. readingsSingleUpdate($hash, $prefix."Duration", "", 1);
  955. } else {
  956. eval {
  957. $xml = XMLin($metaData->{val}, KeepRoot => 1, ForceArray => [], KeyAttr => []);
  958. Log3 $hash, 4, "DLNARenderer: MetaData: ".Dumper($xml);
  959. };
  960. if(!$@) {
  961. DLNARenderer_updateMetaDataItemPart($hash, $prefix."Title", $xml->{"DIDL-Lite"}{item}{"dc:title"});
  962. DLNARenderer_updateMetaDataItemPart($hash, $prefix."Artist", $xml->{"DIDL-Lite"}{item}{"dc:creator"});
  963. DLNARenderer_updateMetaDataItemPart($hash, $prefix."Album", $xml->{"DIDL-Lite"}{item}{"upnp:album"});
  964. DLNARenderer_updateMetaDataItemPart($hash, $prefix."AlbumArtist", $xml->{"DIDL-Lite"}{item}{"r:albumArtist"});
  965. if($xml->{"DIDL-Lite"}{item}{"upnp:albumArtURI"}) {
  966. DLNARenderer_updateMetaDataItemPart($hash, $prefix."AlbumArtURI", $xml->{"DIDL-Lite"}{item}{"upnp:albumArtURI"});
  967. } else {
  968. readingsSingleUpdate($hash, $prefix."AlbumArtURI", "", 1);
  969. }
  970. DLNARenderer_updateMetaDataItemPart($hash, $prefix."OriginalTrackNumber", $xml->{"DIDL-Lite"}{item}{"upnp:originalTrackNumber"});
  971. if($xml->{"DIDL-Lite"}{item}{res}) {
  972. DLNARenderer_updateMetaDataItemPart($hash, $prefix."Duration", $xml->{"DIDL-Lite"}{item}{res}{duration});
  973. } else {
  974. readingsSingleUpdate($hash, $prefix."Duration", "", 1);
  975. }
  976. } else {
  977. Log3 $hash, 1, "DLNARenderer: XML parsing error: ".$@;
  978. }
  979. }
  980. }
  981. return undef;
  982. }
  983. sub DLNARenderer_updateMetaDataItemPart {
  984. my ($hash, $readingName, $item) = @_;
  985. my $currVal = ReadingsVal($hash->{NAME}, $readingName, "");
  986. if($item) {
  987. $item = "" if(ref $item eq ref {});
  988. if($currVal ne $item) {
  989. readingsSingleUpdate($hash, $readingName, $item, 1);
  990. }
  991. }
  992. return undef;
  993. }
  994. ##############################
  995. ####### DISCOVERY ############
  996. ##############################
  997. sub DLNARenderer_setupControlpoint {
  998. my ($hash) = @_;
  999. my $error;
  1000. my $cp;
  1001. my @usedonlyIPs = split(/,/, AttrVal($hash->{NAME}, 'usedonlyIPs', ''));
  1002. my @ignoredIPs = split(/,/, AttrVal($hash->{NAME}, 'ignoredIPs', ''));
  1003. do {
  1004. eval {
  1005. $cp = UPnP::ControlPoint->new(SearchPort => 0, SubscriptionPort => 0, MaxWait => 30, UsedOnlyIP => \@usedonlyIPs, IgnoreIP => \@ignoredIPs, LogLevel => AttrVal($hash->{NAME}, 'verbose', 0));
  1006. $hash->{helper}{controlpoint} = $cp;
  1007. DLNARenderer_addSocketsToMainloop($hash);
  1008. };
  1009. $error = $@;
  1010. } while($error);
  1011. return undef;
  1012. }
  1013. sub DLNARenderer_startDlnaRendererSearch {
  1014. my ($hash) = @_;
  1015. eval {
  1016. $hash->{helper}{controlpoint}->searchByType('urn:schemas-upnp-org:device:MediaRenderer:1', sub { DLNARenderer_discoverCallback($hash, @_); });
  1017. };
  1018. if($@) {
  1019. Log3 $hash, 2, "DLNARenderer: Search failed with error $@";
  1020. }
  1021. return undef;
  1022. }
  1023. sub DLNARenderer_discoverCallback {
  1024. my ($hash, $search, $device, $action) = @_;
  1025. Log3 $hash, 4, "DLNARenderer: $action, ".$device->friendlyName();
  1026. if($action eq "deviceAdded") {
  1027. DLNARenderer_addedDevice($hash, $device);
  1028. } elsif($action eq "deviceRemoved") {
  1029. DLNARenderer_removedDevice($hash, $device);
  1030. }
  1031. return undef;
  1032. }
  1033. sub DLNARenderer_subscriptionCallback {
  1034. my ($hash, $service, %properties) = @_;
  1035. Log3 $hash, 4, "DLNARenderer: Received event: ".Dumper(%properties);
  1036. foreach my $property (keys %properties) {
  1037. $properties{$property} = decode_entities($properties{$property});
  1038. my $xml;
  1039. eval {
  1040. if($properties{$property} =~ /xml/) {
  1041. $xml = XMLin($properties{$property}, KeepRoot => 1, ForceArray => [qw(Volume Mute Loudness VolumeDB group)], KeyAttr => []);
  1042. } else {
  1043. $xml = $properties{$property};
  1044. }
  1045. };
  1046. if($@) {
  1047. Log3 $hash, 2, "DLNARenderer: XML formatting error: ".$@.", ".$properties{$property};
  1048. next;
  1049. }
  1050. DLNARenderer_processEventXml($hash, $property, $xml);
  1051. }
  1052. return undef;
  1053. }
  1054. sub DLNARenderer_renewSubscriptions {
  1055. my ($hash) = @_;
  1056. my $dev = $hash->{helper}{device};
  1057. InternalTimer(gettimeofday() + 200, 'DLNARenderer_renewSubscriptions', $hash, 0);
  1058. return undef if(!defined($dev));
  1059. BlockingCall('DLNARenderer_renewSubscriptionBlocking', $hash->{NAME});
  1060. return undef;
  1061. }
  1062. sub DLNARenderer_renewSubscriptionBlocking {
  1063. my ($string) = @_;
  1064. my ($name) = split("\\|", $string);
  1065. my $hash = $main::defs{$name};
  1066. $SIG{__WARN__} = sub {
  1067. my ($called_from) = caller(0);
  1068. my $wrn_text = shift;
  1069. Log3 $hash, 5, "DLNARenderer: ".$called_from.", ".$wrn_text;
  1070. };
  1071. #register callbacks
  1072. #urn:upnp-org:serviceId:AVTransport
  1073. eval {
  1074. if(defined($hash->{helper}{avTransportSubscription})) {
  1075. $hash->{helper}{avTransportSubscription}->renew();
  1076. }
  1077. };
  1078. #urn:upnp-org:serviceId:RenderingControl
  1079. eval {
  1080. if(defined($hash->{helper}{renderingControlSubscription})) {
  1081. $hash->{helper}{renderingControlSubscription}->renew();
  1082. }
  1083. };
  1084. #urn:pure-com:serviceId:SpeakerManagement
  1085. eval {
  1086. if(defined($hash->{helper}{speakerManagementSubscription})) {
  1087. $hash->{helper}{speakerManagementSubscription}->renew();
  1088. }
  1089. };
  1090. }
  1091. sub DLNARenderer_addedDevice {
  1092. my ($hash, $dev) = @_;
  1093. my $udn = $dev->UDN();
  1094. #TODO check for BOSE UDN
  1095. #ignoreUDNs
  1096. return undef if(AttrVal($hash->{NAME}, "ignoreUDNs", "") =~ /$udn/);
  1097. #acceptedUDNs
  1098. my $acceptedUDNs = AttrVal($hash->{NAME}, "acceptedUDNs", "");
  1099. return undef if($acceptedUDNs ne "" && $acceptedUDNs !~ /$udn/);
  1100. my $foundDevice = 0;
  1101. my @allDLNARenderers = DLNARenderer_getAllDLNARenderers($hash);
  1102. foreach my $DLNARendererHash (@allDLNARenderers) {
  1103. if($DLNARendererHash->{UDN} eq $dev->UDN()) {
  1104. $foundDevice = 1;
  1105. }
  1106. }
  1107. if(!$foundDevice) {
  1108. my $uniqueDeviceName = "DLNA_".substr($dev->UDN(),29,12);
  1109. if(length($uniqueDeviceName) < 17) {
  1110. $uniqueDeviceName = "DLNA_".substr($dev->UDN(),5);
  1111. $uniqueDeviceName =~ tr/-/_/;
  1112. }
  1113. CommandDefine(undef, "$uniqueDeviceName DLNARenderer ".$dev->UDN());
  1114. CommandAttr(undef,"$uniqueDeviceName alias ".$dev->friendlyName());
  1115. CommandAttr(undef,"$uniqueDeviceName webCmd volume");
  1116. if(AttrVal($hash->{NAME}, "defaultRoom", "") ne "") {
  1117. CommandAttr(undef,"$uniqueDeviceName room ".AttrVal($hash->{NAME}, "defaultRoom", ""));
  1118. }
  1119. Log3 $hash, 3, "DLNARenderer: Created device $uniqueDeviceName for ".$dev->friendlyName();
  1120. #update list
  1121. @allDLNARenderers = DLNARenderer_getAllDLNARenderers($hash);
  1122. }
  1123. foreach my $DLNARendererHash (@allDLNARenderers) {
  1124. if($DLNARendererHash->{UDN} eq $dev->UDN()) {
  1125. #device found, update data
  1126. $DLNARendererHash->{helper}{device} = $dev;
  1127. #update device information (FIXME only on change)
  1128. readingsSingleUpdate($DLNARendererHash, "friendlyName", $dev->friendlyName(), 1);
  1129. readingsSingleUpdate($DLNARendererHash, "manufacturer", $dev->manufacturer(), 1);
  1130. readingsSingleUpdate($DLNARendererHash, "modelDescription", $dev->modelDescription(), 1);
  1131. readingsSingleUpdate($DLNARendererHash, "modelName", $dev->modelName(), 1);
  1132. readingsSingleUpdate($DLNARendererHash, "modelNumber", $dev->modelNumber(), 1);
  1133. readingsSingleUpdate($DLNARendererHash, "modelURL", $dev->modelURL(), 1);
  1134. readingsSingleUpdate($DLNARendererHash, "manufacturerURL", $dev->manufacturerURL(), 1);
  1135. readingsSingleUpdate($DLNARendererHash, "presentationURL", $dev->presentationURL(), 1);
  1136. readingsSingleUpdate($DLNARendererHash, "manufacturer", $dev->manufacturer(), 1);
  1137. #register callbacks
  1138. #urn:upnp-org:serviceId:AVTransport
  1139. if(DLNARenderer_upnpGetService($DLNARendererHash, "AVTransport")) {
  1140. $DLNARendererHash->{helper}{avTransportSubscription} = DLNARenderer_upnpGetService($DLNARendererHash, "AVTransport")->subscribe(sub { DLNARenderer_subscriptionCallback($DLNARendererHash, @_); }, 1);
  1141. }
  1142. #urn:upnp-org:serviceId:RenderingControl
  1143. if(DLNARenderer_upnpGetService($DLNARendererHash, "RenderingControl")) {
  1144. $DLNARendererHash->{helper}{renderingControlSubscription} = DLNARenderer_upnpGetService($DLNARendererHash, "RenderingControl")->subscribe(sub { DLNARenderer_subscriptionCallback($DLNARendererHash, @_); }, 1);
  1145. }
  1146. #urn:pure-com:serviceId:SpeakerManagement
  1147. if(DLNARenderer_upnpGetService($DLNARendererHash, "SpeakerManagement")) {
  1148. $DLNARendererHash->{helper}{speakerManagementSubscription} = DLNARenderer_upnpGetService($DLNARendererHash, "SpeakerManagement")->subscribe(sub { DLNARenderer_subscriptionCallback($DLNARendererHash, @_); }, 1);
  1149. }
  1150. #set online
  1151. readingsSingleUpdate($DLNARendererHash,"presence","online",1);
  1152. if(ReadingsVal($DLNARendererHash->{NAME}, "state", "") eq "offline") {
  1153. readingsSingleUpdate($DLNARendererHash,"state","online",1);
  1154. }
  1155. #check caskeid
  1156. if(DLNARenderer_upnpGetService($DLNARendererHash, "SessionManagement")) {
  1157. $DLNARendererHash->{helper}{caskeid} = 1;
  1158. readingsSingleUpdate($DLNARendererHash,"multiRoomSupport","1",1);
  1159. } else {
  1160. readingsSingleUpdate($DLNARendererHash,"multiRoomSupport","0",1);
  1161. }
  1162. #update list of caskeid clients
  1163. my @caskeidClients = DLNARenderer_getAllDLNARenderersWithCaskeid($hash);
  1164. $DLNARendererHash->{helper}{caskeidClients} = "";
  1165. foreach my $client (@caskeidClients) {
  1166. #do not add myself
  1167. if($client->{UDN} ne $DLNARendererHash->{UDN}) {
  1168. $DLNARendererHash->{helper}{caskeidClients} .= ",".ReadingsVal($client->{NAME}, "friendlyName", "");
  1169. }
  1170. }
  1171. $DLNARendererHash->{helper}{caskeidClients} = substr($DLNARendererHash->{helper}{caskeidClients}, 1) if($DLNARendererHash->{helper}{caskeidClients} ne "");
  1172. InternalTimer(gettimeofday() + 200, 'DLNARenderer_renewSubscriptions', $DLNARendererHash, 0);
  1173. InternalTimer(gettimeofday() + 60, 'DLNARenderer_updateStereoMode', $DLNARendererHash, 0);
  1174. }
  1175. }
  1176. return undef;
  1177. }
  1178. sub DLNARenderer_removedDevice($$) {
  1179. my ($hash, $device) = @_;
  1180. my $deviceHash = DLNARenderer_getHashByUDN($hash, $device->UDN());
  1181. return undef if(!defined($deviceHash));
  1182. readingsSingleUpdate($deviceHash, "presence", "offline", 1);
  1183. readingsSingleUpdate($deviceHash, "state", "offline", 1);
  1184. RemoveInternalTimer($deviceHash, 'DLNARenderer_renewSubscriptions');
  1185. RemoveInternalTimer($deviceHash, 'DLNARenderer_updateStereoMode');
  1186. }
  1187. ###############################
  1188. ##### GET PLAYER FUNCTIONS ####
  1189. ###############################
  1190. sub DLNARenderer_getMainDLNARenderer($) {
  1191. my ($hash) = @_;
  1192. foreach my $fhem_dev (sort keys %main::defs) {
  1193. return $main::defs{$fhem_dev} if($main::defs{$fhem_dev}{TYPE} eq 'DLNARenderer' && $main::defs{$fhem_dev}{UDN} eq "0");
  1194. }
  1195. return undef;
  1196. }
  1197. sub DLNARenderer_getHashByUDN($$) {
  1198. my ($hash, $udn) = @_;
  1199. foreach my $fhem_dev (sort keys %main::defs) {
  1200. return $main::defs{$fhem_dev} if($main::defs{$fhem_dev}{TYPE} eq 'DLNARenderer' && $main::defs{$fhem_dev}{UDN} eq $udn);
  1201. }
  1202. return undef;
  1203. }
  1204. sub DLNARenderer_getHashByFriendlyName {
  1205. my ($hash, $friendlyName) = @_;
  1206. foreach my $fhem_dev (sort keys %main::defs) {
  1207. my $devHash = $main::defs{$fhem_dev};
  1208. return $devHash if($devHash->{TYPE} eq 'DLNARenderer' && ReadingsVal($devHash->{NAME}, "friendlyName", "") eq $friendlyName);
  1209. }
  1210. return undef;
  1211. }
  1212. sub DLNARenderer_getAllDLNARenderers($) {
  1213. my ($hash) = @_;
  1214. my @DLNARenderers = ();
  1215. foreach my $fhem_dev (sort keys %main::defs) {
  1216. push @DLNARenderers, $main::defs{$fhem_dev} if($main::defs{$fhem_dev}{TYPE} eq 'DLNARenderer' && $main::defs{$fhem_dev}{UDN} ne "0" && $main::defs{$fhem_dev}{UDN} ne "-1");
  1217. }
  1218. return @DLNARenderers;
  1219. }
  1220. sub DLNARenderer_getAllDLNARenderersWithCaskeid($) {
  1221. my ($hash) = @_;
  1222. my @caskeidClients = ();
  1223. my @DLNARenderers = DLNARenderer_getAllDLNARenderers($hash);
  1224. foreach my $DLNARenderer (@DLNARenderers) {
  1225. push @caskeidClients, $DLNARenderer if($DLNARenderer->{helper}{caskeid});
  1226. }
  1227. return @caskeidClients;
  1228. }
  1229. ###############################
  1230. ###### UTILITY FUNCTIONS ######
  1231. ###############################
  1232. sub DLNARenderer_generateDidlLiteAndPlay {
  1233. my ($hash, $stream) = @_;
  1234. BlockingCall('DLNARenderer_generateDidlLiteBlocking', $hash->{NAME}."|".$stream, 'DLNARenderer_generateDidlLiteBlockingFinished');
  1235. return undef;
  1236. }
  1237. sub DLNARenderer_generateDidlLiteBlockingFinished {
  1238. my ($string) = @_;
  1239. return unless (defined($string));
  1240. my ($name, $stream, $meta) = split("\\|",$string);
  1241. my $hash = $defs{$name};
  1242. DLNARenderer_upnpSetAVTransportURI($hash, $stream, $meta);
  1243. DLNARenderer_play($hash);
  1244. readingsSingleUpdate($hash, "stream", $stream, 1);
  1245. }
  1246. sub DLNARenderer_generateDidlLiteBlocking {
  1247. my ($string) = @_;
  1248. my ($name, $stream) = split("\\|", $string);
  1249. my $hash = $main::defs{$name};
  1250. my $ret = $name."|".$stream;
  1251. if(index($stream, "http:") != 0) {
  1252. return $ret;
  1253. }
  1254. my $ua = new LWP::UserAgent(agent => 'Mozilla/5.0 (Windows NT 6.1; WOW64; rv:40.0) Gecko/20100101 Firefox/40.1');
  1255. $ua->max_size(0);
  1256. my $resp = $ua->get($stream);
  1257. my $didl_header = '<DIDL-Lite xmlns="urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/" xmlns:upnp="urn:schemas-upnp-org:metadata-1-0/upnp/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:dlna="urn:schemas-dlna-org:metadata-1-0/" xmlns:sec="http://www.sec.co.kr/"><item id="-1" parentID="parent" restricted="1">';
  1258. my $didl_footer = '</item></DIDL-Lite>';
  1259. $stream = encode_entities($stream);
  1260. my $size = "";
  1261. my $protocolInfo = "";
  1262. my $album = $stream;
  1263. my $title = $stream;
  1264. my $meta = "";
  1265. if (defined($resp->header('content-length'))) {
  1266. $size = ' size="'.$resp->header('content-length').'"';
  1267. }
  1268. my @header = split /;/, $resp->header('content-type');
  1269. my $contenttype = $header[0];
  1270. if (defined($resp->header('contentfeatures.dlna.org'))) {
  1271. $protocolInfo = "http-get:*:".$contenttype.":".$resp->header('contentfeatures.dlna.org');
  1272. } else {
  1273. $protocolInfo = "http-get:*:".$contenttype.":DLNA.ORG_OP=01;DLNA.ORG_FLAGS=01700000000000000000000000000000";
  1274. }
  1275. if (defined($resp->header('icy-name'))) {
  1276. $album = encode_entities($resp->header('icy-name'));
  1277. }
  1278. if (defined($resp->header('icy-genre'))) {
  1279. $title = encode_entities($resp->header('icy-genre'));
  1280. }
  1281. if (substr($contenttype,0,5) eq "audio" or $contenttype eq "application/ogg") {
  1282. $meta = $didl_header.'<upnp:class>object.item.audioItem.musicTrack</upnp:class><dc:title>'.$title.'</dc:title><upnp:album>'.$album.'</upnp:album><res protocolInfo="'.$protocolInfo.'"'.$size.'>'.$stream.'</res>'.$didl_footer;
  1283. } elsif (substr($contenttype,0,5) eq "video") {
  1284. $meta = $didl_header.'<upnp:class>object.item.videoItem</upnp:class><dc:title>'.$title.'</dc:title><upnp:album>'.$album.'</upnp:album><res protocolInfo="'.$protocolInfo.'"'.$size.'>'.$stream.'</res>'.$didl_footer;
  1285. } else {
  1286. $meta = "";
  1287. }
  1288. $ret .= "|".$meta;
  1289. return $ret;
  1290. }
  1291. sub DLNARenderer_newChash($$$) {
  1292. my ($hash,$socket,$chash) = @_;
  1293. $chash->{TYPE} = $hash->{TYPE};
  1294. $chash->{UDN} = -1;
  1295. $chash->{NR} = $devcount++;
  1296. $chash->{phash} = $hash;
  1297. $chash->{PNAME} = $hash->{NAME};
  1298. $chash->{CD} = $socket;
  1299. $chash->{FD} = $socket->fileno();
  1300. $chash->{PORT} = $socket->sockport if( $socket->sockport );
  1301. $chash->{TEMPORARY} = 1;
  1302. $attr{$chash->{NAME}}{room} = 'hidden';
  1303. $defs{$chash->{NAME}} = $chash;
  1304. $selectlist{$chash->{NAME}} = $chash;
  1305. }
  1306. sub DLNARenderer_closeSocket($) {
  1307. my ($hash) = @_;
  1308. my $name = $hash->{NAME};
  1309. RemoveInternalTimer($hash);
  1310. close($hash->{CD});
  1311. delete($hash->{CD});
  1312. delete($selectlist{$name});
  1313. delete($hash->{FD});
  1314. }
  1315. sub DLNARenderer_addSocketsToMainloop {
  1316. my ($hash) = @_;
  1317. my @sockets = $hash->{helper}{controlpoint}->sockets();
  1318. #check if new sockets need to be added to mainloop
  1319. foreach my $s (@sockets) {
  1320. #create chash and add to selectlist
  1321. my $chash = DLNARenderer_newChash($hash, $s, {NAME => "DLNASocket-".$hash->{NAME}."-".$s->fileno()});
  1322. }
  1323. return undef;
  1324. }
  1325. 1;
  1326. =pod
  1327. =item device
  1328. =item summary Autodiscover and control your DLNA renderer devices easily
  1329. =item summary_DE Autodiscover und einfache Steuerung deiner DLNA Renderer Geräte
  1330. =begin html
  1331. <a name="DLNARenderer"></a>
  1332. <h3>DLNARenderer</h3>
  1333. <ul>
  1334. DLNARenderer automatically discovers all your MediaRenderer devices in your local network and allows you to fully control them.<br>
  1335. It also supports multiroom audio for Caskeid and Bluetooth Caskeid speakers (e.g. MUNET).<br><br>
  1336. <b>Note:</b> The followig libraries are required for this module:
  1337. <ul><li>SOAP::Lite</li> <li>LWP::Simple</li> <li>XML::Simple</li> <li>XML::Parser::Lite</li> <li>LWP::UserAgent</li><br>
  1338. </ul>
  1339. <a name="DLNARendererdefine"></a>
  1340. <b>Define</b>
  1341. <ul>
  1342. <code>define &lt;name&gt; DLNARenderer</code>
  1343. <br><br>
  1344. Example:
  1345. <ul>
  1346. <code>define dlnadevices DLNARenderer</code><br>
  1347. After about 2 minutes you can find all automatically created DLNA devices under "Unsorted".<br/>
  1348. </ul>
  1349. </ul>
  1350. <br>
  1351. <a name="DLNARendererset"></a>
  1352. <b>Set</b>
  1353. <ul>
  1354. <br><code>set &lt;name&gt; stream &lt;value&gt</code><br>
  1355. Set any URL to play.
  1356. </ul>
  1357. <ul>
  1358. <br><code>set &lt;name&gt; on</code><br>
  1359. Starts playing the last stream (reading stream).
  1360. </ul>
  1361. <ul>
  1362. <br><code>set &lt;name&gt; off</code><br>
  1363. Sends stop command to device.
  1364. </ul>
  1365. <ul>
  1366. <br><code>set &lt;name&gt; stop</code><br>
  1367. Stop playback.
  1368. </ul>
  1369. <ul>
  1370. <br><code>set &lt;name&gt; volume 0-100</code><br>
  1371. <code>set &lt;name&gt; volume +/-0-100</code><br>
  1372. Set volume of the device.
  1373. </ul>
  1374. <ul>
  1375. <br><code>set &lt;name&gt; channel 1-10</code><br>
  1376. Start playing channel X which must be configured as channel_X attribute first.<br>
  1377. You can specify your channel also in DIDL-Lite XML format if your player doesn't support plain URIs.
  1378. </ul>
  1379. <ul>
  1380. <br><code>set &lt;name&gt; mute on/off</code><br>
  1381. Mute the device.
  1382. </ul>
  1383. <ul>
  1384. <br><code>set &lt;name&gt; pause</code><br>
  1385. Pause playback of the device. No toggle.
  1386. </ul>
  1387. <ul>
  1388. <br><code>set &lt;name&gt; pauseToggle</code><br>
  1389. Toggle pause/play for the device.
  1390. </ul>
  1391. <ul>
  1392. <br><code>set &lt;name&gt; play</code><br>
  1393. Initiate play command. Only makes your player play if a stream was loaded (currentTrackURI is set).
  1394. </ul>
  1395. <ul>
  1396. <br><code>set &lt;name&gt; next</code><br>
  1397. Play next track.
  1398. </ul>
  1399. <ul>
  1400. <br><code>set &lt;name&gt; previous</code><br>
  1401. Play previous track.
  1402. </ul>
  1403. <ul>
  1404. <br><code>set &lt;name&gt; seek &lt;seconds&gt;</code><br>
  1405. Seek to position of track in seconds.
  1406. </ul>
  1407. <ul>
  1408. <br><code>set &lt;name&gt; speak "This is a test. 1 2 3."</code><br>
  1409. Speak the text followed after speak within quotes. Works with Google Translate.
  1410. </ul>
  1411. <ul>
  1412. <br><code>set &lt;name&gt; playEverywhere</code><br>
  1413. Only available for Caskeid players.<br>
  1414. Play current track on all available Caskeid players in sync.
  1415. </ul>
  1416. <ul>
  1417. <br><code>set &lt;name&gt; stopPlayEverywhere</code><br>
  1418. Only available for Caskeid players.<br>
  1419. Stops multiroom audio.
  1420. </ul>
  1421. <ul>
  1422. <br><code>set &lt;name&gt; addUnit &lt;unitName&gt;</code><br>
  1423. Only available for Caskeid players.<br>
  1424. Adds unit to multiroom audio session.
  1425. </ul>
  1426. <ul>
  1427. <br><code>set &lt;name&gt; removeUnit &lt;unitName&gt;</code><br>
  1428. Only available for Caskeid players.<br>
  1429. Removes unit from multiroom audio session.
  1430. </ul>
  1431. <ul>
  1432. <br><code>set &lt;name&gt; multiRoomVolume 0-100</code><br>
  1433. <code>set &lt;name&gt; multiRoomVolume +/-0-100</code><br>
  1434. Only available for Caskeid players.<br>
  1435. Set volume of all devices within this session.
  1436. </ul>
  1437. <ul>
  1438. <br><code>set &lt;name&gt; enableBTCaskeid</code><br>
  1439. Only available for Caskeid players.<br>
  1440. Activates Bluetooth Caskeid for this device.
  1441. </ul>
  1442. <ul>
  1443. <br><code>set &lt;name&gt; disableBTCaskeid</code><br>
  1444. Only available for Caskeid players.<br>
  1445. Deactivates Bluetooth Caskeid for this device.
  1446. </ul>
  1447. <ul>
  1448. <br><code>set &lt;name&gt; stereo &lt;left&gt; &lt;right&gt; &lt;pairName&gt;</code><br>
  1449. Only available for Caskeid players.<br>
  1450. Sets stereo mode for left/right speaker and defines the name of the stereo pair.
  1451. </ul>
  1452. <ul>
  1453. <br><code>set &lt;name&gt; standalone</code><br>
  1454. Only available for Caskeid players.<br>
  1455. Puts the speaker into standalone mode if it was member of a stereo pair before.
  1456. </ul>
  1457. <ul>
  1458. <br><code>set &lt;name&gt; saveGroupAs &lt;groupName&gt;</code><br>
  1459. Only available for Caskeid players.<br>
  1460. Saves the current group configuration (e.g. saveGroupAs LivingRoom).
  1461. </ul>
  1462. <ul>
  1463. <br><code>set &lt;name&gt; loadGroup &lt;groupName&gt;</code><br>
  1464. Only available for Caskeid players.<br>
  1465. Loads the configuration previously saved (e.g. loadGroup LivingRoom).
  1466. </ul>
  1467. <br>
  1468. <a name="DLNARendererattr"></a>
  1469. <b>Attributes</b>
  1470. <ul>
  1471. <br><code>ignoreUDNs</code><br>
  1472. Define list (comma or blank separated) of UDNs which should prevent automatic device creation.<br>
  1473. It is important that uuid: is also part of the UDN and must be included.
  1474. </ul>
  1475. </ul>
  1476. =end html
  1477. =cut