ControlPoint.pm 44 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704
  1. ########################################################################################
  2. #
  3. # ControlPoint.pm
  4. #
  5. # $Id: ControlPoint.pm 15823 2018-01-07 22:42:45Z Reinerlein $
  6. #
  7. # Now (in this version) part of Fhem.
  8. #
  9. # Fhem is free software: you can redistribute it and/or modify
  10. # it under the terms of the GNU General Public License as published by
  11. # the Free Software Foundation, either version 2 of the License, or
  12. # (at your option) any later version.
  13. #
  14. # Fhem is distributed in the hope that it will be useful,
  15. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  16. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  17. # GNU General Public License for more details.
  18. #
  19. # You should have received a copy of the GNU General Public License
  20. # along with fhem. If not, see <http://www.gnu.org/licenses/>.
  21. #
  22. ########################################################################################
  23. package UPnP::ControlPoint;
  24. use 5.006;
  25. use strict;
  26. use warnings;
  27. use utf8;
  28. use Carp;
  29. use IO::Socket::INET;
  30. use Socket;
  31. use IO::Select;
  32. use HTTP::Daemon;
  33. use HTTP::Headers;
  34. use LWP::UserAgent;
  35. use UPnP::Common;
  36. use vars qw($VERSION @ISA);
  37. require Exporter;
  38. our @ISA = qw(Exporter UPnP::Common::DeviceLoader);
  39. our $VERSION = $UPnP::Common::VERSION;
  40. use constant DEFAULT_SSDP_SEARCH_PORT => 8008;
  41. use constant DEFAULT_SUBSCRIPTION_PORT => 8058;
  42. use constant DEFAULT_SUBSCRIPTION_URL => '/eventSub';
  43. our @IGNOREIP;
  44. our @USEDONLYIP;
  45. our $LogLevel;
  46. sub isIgnoreIP($) {
  47. my($ip) = @_;
  48. foreach my $elem (@IGNOREIP) {
  49. if ($elem =~ m/^\/(.*)\/$/) {
  50. if ($ip =~ m/^$1$/) {
  51. return 1;
  52. }
  53. } else {
  54. if ($ip eq $elem) {
  55. return 1;
  56. }
  57. }
  58. }
  59. return 0;
  60. }
  61. sub isUsedOnlyIP($) {
  62. my($ip) = @_;
  63. return 1 if (!scalar(@USEDONLYIP));
  64. foreach my $elem (@USEDONLYIP) {
  65. if ($elem =~ m/^\/(.*)\/$/) {
  66. if ($ip =~ m/^$1$/) {
  67. return 1;
  68. }
  69. } else {
  70. if ($ip eq $elem) {
  71. return 1;
  72. }
  73. }
  74. }
  75. return 0;
  76. }
  77. sub new {
  78. my($self, %args) = @_;
  79. my $class = ref($self) || $self;
  80. $self = $class->SUPER::new(%args);
  81. my $searchPort = defined($args{SearchPort}) ? $args{SearchPort} : DEFAULT_SSDP_SEARCH_PORT;
  82. my $subscriptionPort = defined($args{SubscriptionPort}) ? $args{SubscriptionPort} : DEFAULT_SUBSCRIPTION_PORT;
  83. my $maxWait = $args{MaxWait} || 3;
  84. @IGNOREIP = @{$args{IgnoreIP}};
  85. @USEDONLYIP = @{$args{UsedOnlyIP}};
  86. $LogLevel = $args{LogLevel} || 0;
  87. my $reuseport = $args{ReusePort};
  88. $reuseport = 0 if (!defined($reuseport));
  89. # Create the socket on which search requests go out
  90. $self->{_searchSocket} = IO::Socket::INET->new(Proto => 'udp', LocalPort => $searchPort) || carp("Error creating search socket: $!\n");
  91. setsockopt($self->{_searchSocket},
  92. IP_LEVEL,
  93. IP_MULTICAST_TTL,
  94. pack 'I', 4);
  95. $self->{_maxWait} = $maxWait;
  96. # Create the socket on which we'll listen for events to which we are
  97. # subscribed.
  98. $self->{_subscriptionSocket} = HTTP::Daemon->new(LocalPort => $subscriptionPort, Reuse=>1, Listen=>20) || carp("Error creating subscription socket: $!\n");
  99. $self->{_subscriptionURL} = $args{SubscriptionURL} || DEFAULT_SUBSCRIPTION_URL;
  100. $self->{_subscriptionPort} = $self->{_subscriptionSocket}->sockport();;
  101. # Create the socket on which we'll listen for SSDP Notifications.
  102. # First try with ReusePort (if given as parameter)...
  103. eval {
  104. $self->{_ssdpMulticastSocket} = IO::Socket::INET->new(
  105. Proto => 'udp',
  106. Reuse => 1,
  107. ReusePort => $reuseport,
  108. LocalPort => SSDP_PORT) ||
  109. croak("Error creating SSDP multicast listen socket: $!\n");
  110. };
  111. if ($@ =~ /Your vendor has not defined Socket macro SO_REUSEPORT/i) {
  112. $self->{_ssdpMulticastSocket} = IO::Socket::INET->new(
  113. Proto => 'udp',
  114. Reuse => 1,
  115. LocalPort => SSDP_PORT) ||
  116. croak("Error creating SSDP multicast listen socket: $!\n");
  117. } elsif($@) {
  118. # Weiterwerfen...
  119. croak($@);
  120. }
  121. my $ip_mreq = inet_aton(SSDP_IP) . INADDR_ANY;
  122. setsockopt($self->{_ssdpMulticastSocket},
  123. IP_LEVEL,
  124. IP_ADD_MEMBERSHIP,
  125. $ip_mreq);
  126. setsockopt($self->{_ssdpMulticastSocket},
  127. IP_LEVEL,
  128. IP_MULTICAST_TTL,
  129. pack 'I', 4);
  130. return $self;
  131. }
  132. sub DESTROY {
  133. my $self = shift;
  134. for my $subscription (values %{$self->{_subscriptions}}) {
  135. if ($subscription) {
  136. $subscription->unsubscribe;
  137. }
  138. }
  139. }
  140. sub searchByType {
  141. my $self = shift;
  142. my $type = shift;
  143. my $callback = shift;
  144. my $search = UPnP::ControlPoint::Search->new(Callback => $callback,
  145. Type => $type);
  146. $self->{_activeSearches}->{$search} = $search;
  147. $self->_startSearch($type);
  148. return $search;
  149. }
  150. sub searchByUDN {
  151. my $self = shift;
  152. my $udn = shift;
  153. my $callback = shift;
  154. my $search = UPnP::ControlPoint::Search->new(Callback => $callback,
  155. UDN => $udn);
  156. $self->{_activeSearches}->{$search} = $search;
  157. $self->_startSearch("upnp:rootdevice");
  158. $search;
  159. }
  160. sub searchByFriendlyName {
  161. my $self = shift;
  162. my $name = shift;
  163. my $callback = shift;
  164. my $search = UPnP::ControlPoint::Search->new(Callback => $callback,
  165. FriendlyName => $name);
  166. $self->{_activeSearches}->{$search} = $search;
  167. $self->_startSearch("upnp:rootdevice");
  168. $search;
  169. }
  170. sub stopSearch {
  171. my $self = shift;
  172. my $search = shift;
  173. delete $self->{_activeSearches}->{$search};
  174. }
  175. sub sockets {
  176. my $self = shift;
  177. return ($self->{_subscriptionSocket},
  178. $self->{_ssdpMulticastSocket},
  179. $self->{_searchSocket},);
  180. }
  181. sub handleOnce {
  182. my $self = shift;
  183. my $socket = shift;
  184. if ($socket == $self->{_searchSocket}) {
  185. $self->_receiveSearchResponse($socket);
  186. }
  187. elsif ($socket == $self->{_ssdpMulticastSocket}) {
  188. $self->_receiveSSDPEvent($socket);
  189. }
  190. elsif ($socket == $self->{_subscriptionSocket}) {
  191. if (my $connect = $socket->accept()) {
  192. return if (!isUsedOnlyIP($connect->peerhost()));
  193. return if (isIgnoreIP($connect->peerhost()));
  194. $self->_receiveSubscriptionNotification($connect);
  195. }
  196. }
  197. }
  198. sub handle {
  199. my $self = shift;
  200. my @mysockets = $self->sockets();
  201. my $select = IO::Select->new(@mysockets);
  202. $self->{_handling} = 1;
  203. while ($self->{_handling}) {
  204. my @sockets = $select->can_read(1);
  205. for my $sock (@sockets) {
  206. $self->handleOnce($sock);
  207. }
  208. }
  209. }
  210. sub stopHandling {
  211. my $self = shift;
  212. $self->{_handling} = 0;
  213. }
  214. sub subscriptionURL {
  215. my $self = shift;
  216. return URI->new_abs($self->{_subscriptionURL},
  217. 'http://' . UPnP::Common::getLocalIP() . ':' .
  218. $self->{_subscriptionPort});
  219. }
  220. sub addSubscription {
  221. my $self = shift;
  222. my $subscription = shift;
  223. $self->{_subscriptions}->{$subscription->SID} = $subscription;
  224. }
  225. sub removeSubscription {
  226. my $self = shift;
  227. my $subscription = shift;
  228. delete $self->{_subscriptions}->{$subscription->SID};
  229. }
  230. sub _startSearch {
  231. my $self = shift;
  232. my $target = shift;
  233. my $header = 'M-SEARCH * HTTP/1.1' . CRLF .
  234. 'HOST: ' . SSDP_IP . ':' . SSDP_PORT . CRLF .
  235. 'MAN: "ssdp:discover"' . CRLF .
  236. 'ST: ' . $target . CRLF .
  237. 'MX: ' . $self->{_maxWait} . CRLF .
  238. CRLF;
  239. my $destaddr = sockaddr_in(SSDP_PORT, inet_aton(SSDP_IP));
  240. send($self->{_searchSocket}, $header, 0, $destaddr);
  241. }
  242. sub _parseUSNHeader {
  243. my $usn = shift;
  244. my ($udn, $deviceType, $serviceType);
  245. if ($usn =~ /^uuid:schemas(.*?):device(.*?):(.*?):(.+)$/) {
  246. $udn = 'uuid:' . $4;
  247. $deviceType = 'urn:schemas' . $1 . ':device' . $2 . ':' . $3;
  248. }
  249. elsif ($usn =~ /^uuid:(.+?)::/) {
  250. $udn = 'uuid:' . $1;
  251. if ($usn =~ /urn:(.+)$/) {
  252. my $urn = $1;
  253. if ($usn =~ /:service:/) {
  254. $serviceType = 'urn:' . $urn;
  255. }
  256. elsif ($usn =~ /:device:/) {
  257. $deviceType = 'urn:' . $urn;
  258. }
  259. }
  260. }
  261. else {
  262. $udn = $usn;
  263. }
  264. return ($udn, $deviceType, $serviceType);
  265. }
  266. sub _firstLocation {
  267. my $headers = shift;
  268. my $location = $headers->header('Location');
  269. return $location if $location;
  270. my $al = $headers->header('AL');
  271. if ($al && $al =~ /^<(\S+?)>/) {
  272. return $1;
  273. }
  274. return undef;
  275. }
  276. sub newService {
  277. my $self = shift;
  278. return UPnP::ControlPoint::Service->new(@_);
  279. }
  280. sub newDevice {
  281. my $self = shift;
  282. return UPnP::ControlPoint::Device->new(@_);
  283. }
  284. sub _createDevice {
  285. my $self = shift;
  286. my $location = shift;
  287. my $device;
  288. # We've found examples of where devices claim to do transfer
  289. # encoding, but wind up sending chunks without chunk size headers.
  290. # This code temporarily disables the TE header in the request.
  291. #push(@LWP::Protocol::http::EXTRA_SOCK_OPTS, SendTE => 0);
  292. my @SOCK_OPTS_Backup = @LWP::Protocol::http::EXTRA_SOCK_OPTS;
  293. _addSendTE();
  294. my $ua = LWP::UserAgent->new(timeout => 20);
  295. my $response = $ua->get($location);
  296. my $base;
  297. if ($response->is_success && $response->content ne '') {
  298. ($device, $base) = $self->parseDeviceDescription($response->content,
  299. {Location => $location},
  300. {ControlPoint => $self});
  301. } else {
  302. carp('400-URL-Absolute-Error! Location: "'.$location.'", Content: "'.$response->content.'"') if ($response->code == 400);
  303. carp("Loading device description failed with error: " . $response->code . " " . $response->message . ' (Location: ' . $location . ')') if ($response->code != 200);
  304. }
  305. #pop(@LWP::Protocol::http::EXTRA_SOCK_OPTS);
  306. @LWP::Protocol::http::EXTRA_SOCK_OPTS = @SOCK_OPTS_Backup;
  307. if ($device) {
  308. $device->base($base ? $base : $location);
  309. if ($response->is_success && $response->content ne '') {
  310. $device->descriptionDocument($response->content);
  311. }
  312. }
  313. return $device;
  314. }
  315. sub _addSendTE {
  316. my %arg = @LWP::Protocol::http::EXTRA_SOCK_OPTS;
  317. $arg{SendTE} = 0;
  318. @LWP::Protocol::http::EXTRA_SOCK_OPTS = %arg;
  319. }
  320. sub _getDeviceFromHeaders {
  321. my $self = shift;
  322. my $headers = shift;
  323. my $create = shift;
  324. my $location = _firstLocation($headers);
  325. my ($udn, $deviceType, $serviceType) =
  326. _parseUSNHeader($headers->header('USN'));
  327. my $device = $self->{_devices}->{$udn};
  328. if (!defined($device) && $create) {
  329. $device = $self->_createDevice($location);
  330. if ($device) {
  331. $self->{_devices}->{$udn} = $device;
  332. }
  333. }
  334. return $device;
  335. }
  336. sub _deviceAdded {
  337. my $self = shift;
  338. my $device = shift;
  339. for my $search (values %{$self->{_activeSearches}}) {
  340. $search->deviceAdded($device);
  341. }
  342. }
  343. sub _deviceRemoved {
  344. my $self = shift;
  345. my $device = shift;
  346. for my $search (values %{$self->{_activeSearches}}) {
  347. $search->deviceRemoved($device);
  348. }
  349. }
  350. use Data::Dumper;
  351. sub _receiveSearchResponse {
  352. my $self = shift;
  353. my $socket = shift;
  354. my $buf = '';
  355. my $peer = recv($socket, $buf, 2048, 0);
  356. my @peerdata = unpack_sockaddr_in($peer);
  357. return if (!isUsedOnlyIP(inet_ntoa($peerdata[1])));
  358. return if (isIgnoreIP(inet_ntoa($peerdata[1])));
  359. if ($buf !~ /\015?\012\015?\012/) {
  360. return;
  361. }
  362. $buf =~ s/^(?:\015?\012)+//; # ignore leading blank lines
  363. unless ($buf =~ s/^(\S+)[ \t]+(\S+)[ \t]+(\S+)[^\012]*\012//) {
  364. # Bad header
  365. return;
  366. }
  367. # Basic check to see if the response is actually for a search
  368. my $found = 0;
  369. foreach my $searchkey (keys %{$self->{_activeSearches}}) {
  370. my $search = $self->{_activeSearches}->{$searchkey};
  371. if ($search->{_type} && $buf =~ $search->{_type}) {
  372. print 'xxxx.xx.xx xx:xx:xx 5: ControlPoint: Accepted Search-Response: "'.$buf.'"'."\n" if ($LogLevel >= 5);
  373. $found = 1;
  374. last;
  375. }
  376. if ($search->{_udn} && $buf =~ $search->{_udn}) {
  377. $found = 1;
  378. last;
  379. }
  380. if ($search->{_friendlyName} && $buf =~ $search->{_friendlyName}) {
  381. $found = 1;
  382. last;
  383. }
  384. }
  385. if (! $found) {
  386. print 'xxxx.xx.xx xx:xx:xx 5: ControlPoint: Unknown Search-Response: "'.$buf.'"'."\n" if ($LogLevel >= 5);
  387. return;
  388. }
  389. my $code = $2;
  390. if ($code ne '200') {
  391. # We expect a success response code
  392. return;
  393. }
  394. my $headers = UPnP::Common::parseHTTPHeaders($buf);
  395. my $device = $self->_getDeviceFromHeaders($headers, 1);
  396. if ($device) {
  397. $self->_deviceAdded($device);
  398. }
  399. }
  400. sub _receiveSSDPEvent {
  401. my $self = shift;
  402. my $socket = shift;
  403. my $buf = '';
  404. my $peer = recv($socket, $buf, 2048, 0);
  405. return if (!defined($peer));
  406. my @peerdata = unpack_sockaddr_in($peer);
  407. return if (!@peerdata);
  408. return if (!isUsedOnlyIP(inet_ntoa($peerdata[1])));
  409. return if (isIgnoreIP(inet_ntoa($peerdata[1])));
  410. if ($buf !~ /\015?\012\015?\012/) {
  411. return;
  412. }
  413. $buf =~ s/^(?:\015?\012)+//; # ignore leading blank lines
  414. unless ($buf =~ s/^(\S+)[ \t]+(\S+)(?:[ \t]+(HTTP\/\d+\.\d+))?[^\012]*\012//) {
  415. # Bad header
  416. return;
  417. }
  418. #print Dumper($buf); #ALW uncomment
  419. my $method = $1;
  420. if ($method ne 'NOTIFY') {
  421. # We only care about notifications
  422. return;
  423. }
  424. my $headers = UPnP::Common::parseHTTPHeaders($buf);
  425. my $eventType = $headers->header('NTS');
  426. my $device = $self->_getDeviceFromHeaders($headers,
  427. $eventType =~ /alive/ ?
  428. 1 : 0);
  429. if ($device) {
  430. if ($eventType =~ /alive/) {
  431. $self->_deviceAdded($device);
  432. }
  433. elsif ($eventType =~ /byebye/) {
  434. $self->_deviceRemoved($device);
  435. $self->{_devices}->{$device->UDN()} = undef;
  436. }
  437. }
  438. }
  439. sub _parseProperty {
  440. my $self = shift;
  441. my $element = shift;
  442. my ($name, $attrs, $children) = @$element;
  443. my ($key, $value);
  444. if ($name =~ /property/) {
  445. my $childElement = $children->[0];
  446. $key = $childElement->[0];
  447. $value = $childElement->[2];
  448. }
  449. ($key, $value);
  450. }
  451. sub _parsePropertySet {
  452. my $self = shift;
  453. my $content = shift;
  454. my %properties = ();
  455. my $parser = $self->parser;
  456. my $element = $parser->parse($content);
  457. if (defined($element) && (ref $element eq 'ARRAY') &&
  458. $element->[0] =~ /propertyset/) {
  459. my($name, $attrs, $children) = @$element;
  460. for my $child (@$children) {
  461. my ($key, $value) = $self->_parseProperty($child);
  462. if ($key) {
  463. $properties{$key} = $value;
  464. }
  465. }
  466. }
  467. return %properties;
  468. }
  469. sub _receiveSubscriptionNotification {
  470. my $self = shift;
  471. my $connect = shift;
  472. my $request = $connect->get_request();
  473. if ($request && ($request->method eq 'NOTIFY') &&
  474. ($request->header('NT') eq 'upnp:event') &&
  475. ($request->header('NTS') eq 'upnp:propchange')) {
  476. my $sid = $request->header('SID');
  477. my $subscription = $self->{_subscriptions}->{$sid};
  478. if ($subscription) {
  479. my %propSet = $self->_parsePropertySet($request->content);
  480. $subscription->propChange(%propSet);
  481. }
  482. }
  483. $connect->send_response(HTTP::Response->new(HTTP::Status::RC_OK));
  484. $connect->close;
  485. }
  486. # ----------------------------------------------------------------------
  487. package UPnP::ControlPoint::Device;
  488. use strict;
  489. use vars qw(@ISA);
  490. use UPnP::Common;
  491. our @ISA = qw(UPnP::Common::Device);
  492. sub base {
  493. my $self = shift;
  494. my $base = shift;
  495. if (defined($base)) {
  496. $self->{_base} = $base;
  497. for my $service ($self->services) {
  498. $service->base($base);
  499. }
  500. for my $device ($self->children) {
  501. $device->base($base);
  502. }
  503. }
  504. return $self->{_base};
  505. }
  506. sub descriptionDocument {
  507. my $self = shift;
  508. my $descriptionDocument = shift;
  509. if (defined($descriptionDocument)) {
  510. $self->{_descriptionDocument} = $descriptionDocument;
  511. }
  512. return $self->{_descriptionDocument};
  513. }
  514. # ----------------------------------------------------------------------
  515. package UPnP::ControlPoint::Service;
  516. use strict;
  517. use Socket;
  518. use Scalar::Util qw(weaken);
  519. use SOAP::Lite;
  520. use Carp;
  521. use vars qw($AUTOLOAD @ISA %urlProperties);
  522. use UPnP::Common;
  523. our @ISA = qw(UPnP::Common::Service);
  524. for my $prop (qw(SCPDURL controlURL eventSubURL)) {
  525. $urlProperties{$prop}++;
  526. }
  527. sub new {
  528. my ($self, %args) = @_;
  529. my $class = ref($self) || $self;
  530. $self = $class->SUPER::new(%args);
  531. if ($args{ControlPoint}) {
  532. $self->{_controlPoint} = $args{ControlPoint};
  533. weaken($self->{_controlPoint});
  534. }
  535. return $self;
  536. }
  537. sub AUTOLOAD {
  538. my $self = shift;
  539. my $attr = $AUTOLOAD;
  540. $attr =~ s/.*:://;
  541. return if $attr eq 'DESTROY';
  542. my $superior = "SUPER::$attr";
  543. my $val = $self->$superior(@_);
  544. if ($urlProperties{$attr}) {
  545. my $base = $self->base;
  546. if ($base) {
  547. return URI->new_abs($val, $base);
  548. }
  549. return URI->new($val);
  550. }
  551. return $val;
  552. }
  553. sub controlProxy {
  554. my $self = shift;
  555. $self->_loadDescription;
  556. return UPnP::ControlPoint::ControlProxy->new($self);
  557. }
  558. sub queryStateVariable {
  559. my $self = shift;
  560. my $name = shift;
  561. $self->_loadDescription;
  562. my $var = $self->getStateVariable($name);
  563. if (!$var) { croak("No such state variable $name"); }
  564. if (!$var->evented) { croak("Variable $name is not evented"); }
  565. my $result;
  566. if ($SOAP::Lite::VERSION >= 0.67) {
  567. $result = SOAP::Lite
  568. ->ns("u")
  569. ->uri('urn:schemas-upnp-org:control-1-0')
  570. ->proxy($self->controlURL)
  571. ->call('QueryStateVariable' =>
  572. SOAP::Data->name('varName')
  573. ->uri('urn:schemas-upnp-org:control-1-0')
  574. ->value($name));
  575. } else {
  576. $result = SOAP::Lite
  577. ->uri('urn:schemas-upnp-org:control-1-0')
  578. ->proxy($self->controlURL)
  579. ->call('QueryStateVariable' =>
  580. SOAP::Data->name('varName')
  581. ->uri('urn:schemas-upnp-org:control-1-0')
  582. ->value($name));
  583. }
  584. if ($result->fault()) {
  585. carp("Query failed with fault " . $result->faultstring());
  586. return undef;
  587. }
  588. return $result->result;
  589. }
  590. sub subscribe {
  591. my $self = shift;
  592. my $callback = shift;
  593. my $timeout = shift;
  594. my $cp = $self->{_controlPoint};
  595. if (!defined $UPnP::Common::LocalIP) {
  596. # Find our local IP
  597. my $u = URI->new($self->eventSubURL);
  598. my $proto = getprotobyname('tcp');
  599. socket(Socket_Handle, PF_INET, SOCK_STREAM, $proto);
  600. my $sin = sockaddr_in($u->port(),inet_aton($u->host()));
  601. connect(Socket_Handle,$sin);
  602. my ($port, $addr) = sockaddr_in(getsockname(Socket_Handle));
  603. close(Socket_Handle);
  604. UPnP::Common::setLocalIP($addr);
  605. }
  606. if (defined($cp)) {
  607. my $url = $self->eventSubURL;
  608. my $request = HTTP::Request->new('SUBSCRIBE',
  609. "$url");
  610. $request->header('NT', 'upnp:event');
  611. $request->header('Callback', '<' . $cp->subscriptionURL . '>');
  612. $request->header('Timeout',
  613. 'Second-' . defined($timeout) ? $timeout : 'infinite');
  614. my $ua = LWP::UserAgent->new(timeout => 20);
  615. my $response = $ua->request($request);
  616. if ($response->is_success) {
  617. if ($response->code == 200) {
  618. my $sid = $response->header('SID');
  619. $timeout = $response->header('Timeout');
  620. if ($timeout =~ /^Second-(\d+)$/) {
  621. $timeout = $1;
  622. }
  623. my $subscription = UPnP::ControlPoint::Subscription->new(
  624. Service => $self,
  625. Callback => $callback,
  626. SID => $sid,
  627. Timeout => $timeout,
  628. EventSubURL => "$url");
  629. $cp->addSubscription($subscription);
  630. return $subscription;
  631. } else {
  632. carp("Subscription request successful but answered with error: " . $response->code . " " . $response->message);
  633. }
  634. } else {
  635. carp("Subscription request failed with error: " . $response->code . " " . $response->message);
  636. }
  637. }
  638. return undef;
  639. }
  640. sub unsubscribe {
  641. my $self = shift;
  642. my $subscription = shift;
  643. my $url = $self->eventSubURL;
  644. my $request = HTTP::Request->new('UNSUBSCRIBE',
  645. "$url");
  646. $request->header('SID', $subscription->SID);
  647. my $ua = LWP::UserAgent->new(timeout => 20);
  648. my $response = $ua->request($request);
  649. if ($response->is_success) {
  650. my $cp = $self->{_controlPoint};
  651. if (defined($cp)) {
  652. $cp->removeSubscription($subscription);
  653. }
  654. }
  655. else {
  656. if ($response->code != 412) {
  657. carp("Unsubscription request failed with error: " .
  658. $response->code . " " . $response->message);
  659. }
  660. }
  661. }
  662. sub _loadDescription {
  663. my $self = shift;
  664. if ($self->{_loadedDescription}) {
  665. return;
  666. }
  667. my $location = $self->SCPDURL;
  668. my $cp = $self->{_controlPoint};
  669. unless (defined($location)) {
  670. carp("Service doesn't have a SCPD location");
  671. return;
  672. }
  673. unless (defined($cp)) {
  674. carp("ControlPoint instance no longer exists");
  675. return;
  676. }
  677. my $parser = $cp->parser;
  678. #push(@LWP::Protocol::http::EXTRA_SOCK_OPTS, SendTE => 0);
  679. my @SOCK_OPTS_Backup = @LWP::Protocol::http::EXTRA_SOCK_OPTS;
  680. _addSendTE();
  681. my $ua = LWP::UserAgent->new(timeout => 20);
  682. my $response = $ua->get($location);
  683. if ($response->is_success) {
  684. $self->parseServiceDescription($parser, $response->content);
  685. }
  686. else {
  687. carp("Error loading SCPD document: $!");
  688. }
  689. #pop(@LWP::Protocol::http::EXTRA_SOCK_OPTS);
  690. @LWP::Protocol::http::EXTRA_SOCK_OPTS = @SOCK_OPTS_Backup;
  691. $self->{_loadedDescription} = 1;
  692. }
  693. sub _addSendTE {
  694. my %arg = @LWP::Protocol::http::EXTRA_SOCK_OPTS;
  695. $arg{SendTE} = 0;
  696. @LWP::Protocol::http::EXTRA_SOCK_OPTS = %arg;
  697. }
  698. # ----------------------------------------------------------------------
  699. package UPnP::ControlPoint::ControlProxy;
  700. use strict;
  701. use SOAP::Lite;
  702. use Carp;
  703. use vars qw($AUTOLOAD);
  704. sub new {
  705. my($class, $service) = @_;
  706. if ($SOAP::Lite::VERSION >= 0.67) {
  707. return bless {
  708. _service => $service,
  709. _proxy => SOAP::Lite->ns("u")->uri($service->serviceType)->proxy($service->controlURL),
  710. }, $class;
  711. } else {
  712. return bless {
  713. _service => $service,
  714. _proxy => SOAP::Lite->uri($service->serviceType)->proxy($service->controlURL),
  715. }, $class;
  716. }
  717. }
  718. sub AUTOLOAD {
  719. my $self = shift;
  720. my $service = $self->{_service};
  721. my $proxy = $self->{_proxy};
  722. my $method = $AUTOLOAD;
  723. $method =~ s/.*:://;
  724. return if $method eq 'DESTROY';
  725. my $action = $service->getAction($method);
  726. croak "invalid method: ->$method()" unless $action;
  727. my @inArgs;
  728. for my $arg ($action->inArguments) {
  729. my $val = shift;
  730. my $type = $service->getArgumentType($arg);
  731. push @inArgs, SOAP::Data->type($type => $val)->name($arg->name);
  732. }
  733. return UPnP::ControlPoint::ActionResult->new(
  734. Action => $action,
  735. Service => $service,
  736. SOM => $proxy->call($method => @inArgs));
  737. }
  738. # ----------------------------------------------------------------------
  739. package UPnP::ControlPoint::ActionResult;
  740. use strict;
  741. use SOAP::Lite;
  742. use HTML::Entities ();
  743. use Carp;
  744. use vars qw($AUTOLOAD);
  745. sub new {
  746. my($class, %args) = @_;
  747. my $som = $args{SOM};
  748. my $self = bless {
  749. _som => $som,
  750. }, $class;
  751. unless (defined($som->fault())) {
  752. for my $out ($args{Action}->outArguments) {
  753. my $name = $out->name;
  754. my $data = $som->match('/Envelope/Body//' . $name)->dataof();
  755. if ($data) {
  756. my $type = $args{Service}->getArgumentType($out);
  757. $data->type($type);
  758. if ($type eq 'string') {
  759. $self->{_results}->{$name} = HTML::Entities::decode(
  760. $data->value);
  761. }
  762. else {
  763. $self->{_results}->{$name} = $data->value;
  764. }
  765. }
  766. }
  767. }
  768. return $self;
  769. }
  770. sub isSuccessful {
  771. my $self = shift;
  772. return !defined($self->{_som}->fault());
  773. }
  774. sub getValue {
  775. my $self = shift;
  776. my $name = shift;
  777. if (defined($self->{_results})) {
  778. return $self->{_results}->{$name};
  779. }
  780. return undef;
  781. }
  782. sub AUTOLOAD {
  783. my $self = shift;
  784. my $method = $AUTOLOAD;
  785. $method =~ s/.*:://;
  786. return if $method eq 'DESTROY';
  787. return $self->{_som}->$method(@_);
  788. }
  789. # ----------------------------------------------------------------------
  790. package UPnP::ControlPoint::Search;
  791. use strict;
  792. sub new {
  793. my($class, %args) = @_;
  794. return bless {
  795. _callback => $args{Callback},
  796. _type => $args{Type},
  797. _udn => $args{UDN},
  798. _friendlyName => $args{FriendlyName},
  799. }, $class;
  800. }
  801. sub _passesFilter {
  802. my $self = shift;
  803. my $device = shift;
  804. my $type = $self->{_type};
  805. my $name = $self->{_friendlyName};
  806. my $udn = $self->{_udn};
  807. if ((!defined($type) || ($type eq $device->deviceType()) ||
  808. ($type eq 'ssdp:all')) &&
  809. (!defined($name) || ($name eq $device->friendlyName())) &&
  810. (!defined($udn) || ($udn eq $device->udn()))) {
  811. return 1;
  812. }
  813. return 0;
  814. }
  815. sub deviceAdded {
  816. my $self = shift;
  817. my $device = shift;
  818. if ($self->_passesFilter($device) &&
  819. !$self->{_devices}->{$device}) {
  820. &{$self->{_callback}}($self, $device, 'deviceAdded');
  821. $self->{_devices}->{$device}++;
  822. }
  823. }
  824. sub deviceRemoved {
  825. my $self = shift;
  826. my $device = shift;
  827. if ($self->_passesFilter($device) &&
  828. $self->{_devices}->{$device}) {
  829. &{$self->{_callback}}($self, $device, 'deviceRemoved');
  830. delete $self->{_devices}->{$device};
  831. }
  832. }
  833. # ----------------------------------------------------------------------
  834. package UPnP::ControlPoint::Subscription;
  835. use strict;
  836. use Time::HiRes;
  837. use Scalar::Util qw(weaken);
  838. use Carp;
  839. sub new {
  840. my($class, %args) = @_;
  841. my $self = bless {
  842. _callback => $args{Callback},
  843. _sid => $args{SID},
  844. _timeout => $args{Timeout},
  845. _startTime => Time::HiRes::time(),
  846. _eventSubURL => $args{EventSubURL},
  847. }, $class;
  848. weaken($self->{_service} = $args{Service});
  849. return $self;
  850. }
  851. sub SID {
  852. my $self = shift;
  853. return $self->{_sid};
  854. }
  855. sub timeout {
  856. my $self = shift;
  857. return $self->{_timeout};
  858. }
  859. sub expired {
  860. my $self = shift;
  861. if ($self->{_timeout} eq 'INFINITE') {
  862. return 0;
  863. }
  864. my $now = Time::HiRes::time();
  865. if ($now - $self->{_startTime} > $self->{_timeout}) {
  866. return 1;
  867. }
  868. return 0;
  869. }
  870. sub renew {
  871. my $self = shift;
  872. my $timeout = shift;
  873. my $url = $self->{_eventSubURL};
  874. my $request = HTTP::Request->new('SUBSCRIBE',
  875. "$url");
  876. $request->header('SID', $self->{_sid});
  877. $request->header('Timeout',
  878. 'Second-' . defined($timeout) ? $timeout : 'infinite');
  879. my $ua = LWP::UserAgent->new(timeout => 20);
  880. my $response = $ua->request($request);
  881. if ($response->is_success) {
  882. $timeout = $response->header('Timeout');
  883. if ($timeout =~ /^Second-(\d+)$/) {
  884. $timeout = $1;
  885. }
  886. $self->{_timeout} = $timeout;
  887. $self->{_startTime} = Time::HiRes::time();
  888. }
  889. else {
  890. carp("Renewal of subscription failed with error: " .
  891. $response->code . " " . $response->message);
  892. }
  893. return $self;
  894. }
  895. sub unsubscribe {
  896. my $self = shift;
  897. if ($self->{_service}) {
  898. $self->{_service}->unsubscribe($self);
  899. }
  900. }
  901. sub propChange {
  902. my $self = shift;
  903. my %properties = @_;
  904. if ($self->{_service}) {
  905. &{$self->{_callback}}($self->{_service}, %properties);
  906. }
  907. }
  908. 1;
  909. __END__
  910. =head1 NAME
  911. UPnP::ControlPoint - A UPnP ControlPoint implementation.
  912. =head1 SYNOPSIS
  913. use UPnP::ControlPoint;
  914. my $cp = UPnP::ControlPoint->new;
  915. my $search = $cp->searchByType("urn:schemas-upnp-org:device:TestDevice:1",
  916. \&callback);
  917. $cp->handle;
  918. sub callback {
  919. my ($search, $device, $action) = @_;
  920. if ($action eq 'deviceAdded') {
  921. print("Device: " . $device->friendlyName . " added. Device contains:\n");
  922. for my $service ($device->services) {
  923. print("\tService: " . $service->serviceType . "\n");
  924. }
  925. }
  926. elsif ($action eq 'deviceRemoved') {
  927. print("Device: " . $device->friendlyName . " removed\n");
  928. }
  929. }
  930. =head1 DESCRIPTION
  931. Implements a UPnP ControlPoint. This module implements the various
  932. aspects of the UPnP architecture from the standpoint of a ControlPoint:
  933. =over 4
  934. =item 1. Discovery
  935. A ControlPoint can be used to actively search for devices and services
  936. on a local network or listen for announcements as devices enter and
  937. leave the network. The protocol used for discovery is the Simple
  938. Service Discovery Protocol (SSDP).
  939. =item 2. Description
  940. A ControlPoint can get information describing devices and
  941. services. Devices can be queried for services and vendor-specific
  942. information. Services can be queried for actions and state variables.
  943. =item 3. Control
  944. A ControlPoint can invoke actions on services and poll for state
  945. variable values. Control-related calls are generally made using the
  946. Simple Object Access Protocol (SOAP).
  947. =item 4. Eventing
  948. ControlPoints can listen for events describing state changes in
  949. devices and services. Subscription requests and state change events
  950. are generally sent using the General Event Notification Architecture
  951. (GENA).
  952. =back
  953. Since the UPnP architecture leverages several existing protocols such
  954. as TCP, UDP, HTTP and SOAP, this module requires several Perl modules
  955. that implement these protocols. These include
  956. L<IO::Socket::INET|IO::Socket::INET>,
  957. L<LWP::UserAgent|LWP::UserAgent>,
  958. L<HTTP::Daemon|HTTP::Daemon> and
  959. C<SOAP::Lite> (L<http://www.soaplite.com>).
  960. =head1 METHODS
  961. =head2 UPnP::ControlPoint
  962. A ControlPoint implementor will generally create a single instance of
  963. the C<UPnP::ControlPoint> class (though more than one can exist within
  964. a process assuming that they have been set up to avoid port
  965. conflicts).
  966. =over 4
  967. =item new ( [ARGS] )
  968. Creates a C<UPnP::ControlPoint> object. Accepts the following
  969. key-value pairs as optional arguments (default values are listed
  970. below):
  971. SearchPort Port on which search requests are received 8008
  972. SubscriptionPort Port on which event notification are received 8058
  973. SubscriptionURL URL on which event notification are received /eventSub
  974. MaxWait Max wait before search responses should be sent 3
  975. While this call creates the sockets necessary for the ControlPoint to
  976. function, the ControlPoint is not active until its sockets are
  977. actually serviced, either by invoking the C<handle>
  978. method or by externally selecting using the ControlPoint's
  979. C<sockets> and invoking the
  980. C<handleOnce> method as each becomes ready for
  981. reading.
  982. =item sockets
  983. Returns a list of sockets that need to be serviced for the
  984. ControlPoint to correctly function. This method is generally used in
  985. conjunction with the C<handleOnce> method by users who want to run
  986. their own C<select> loop. This list of sockets should be selected for
  987. reading and C<handleOnce> is invoked for each socket as it beoms ready
  988. for reading.
  989. =item handleOnce ( SOCKET )
  990. Handles the function of reading from a ControlPoint socket when it is
  991. ready (as indicated by a C<select>). This method is used by developers
  992. who want to run their own C<select> loop.
  993. =item handle
  994. Takes over handling of all ControlPoint sockets. Runs its own
  995. C<select> loop, handling individual sockets as they become available
  996. for reading. Returns only when a call to
  997. C<stopHandling> is made (generally from a
  998. ControlPoint callback or a signal handler). This method is an
  999. alternative to using the C<sockets> and
  1000. C<handleOnce> methods.
  1001. =item stopHandling
  1002. Ends the C<select> loop run by C<handle>. This method is generally
  1003. invoked from a ControlPoint callback or a signal handler.
  1004. =item searchByType ( TYPE, CALLBACK )
  1005. Used to start a search for devices on the local network by device or
  1006. service type. The C<TYPE> parameter is a string inidicating a device
  1007. or service type. Specifically, it is the string that will be put into
  1008. the C<ST> header of the SSDP C<M-SEARCH> request that is sent out. The
  1009. C<CALLBACK> parameter is a code reference to a callback that is
  1010. invoked when a device matching the search criterion is found (or a
  1011. SSDP announcement is received that such a device is entering or
  1012. leaving the network). This method returns a
  1013. L<C<UPnP::ControlPoint::Search>|/UPnP::ControlPoint::Search> object.
  1014. The arguments to the C<CALLBACK> are the search object, the device
  1015. that has been found or newly added to or removed from the network, and
  1016. an action string which is one of 'deviceAdded' or 'deviceRemoved'. The
  1017. callback is invoked separately for each device that matches the search
  1018. criterion.
  1019. sub callback {
  1020. my ($search, $device, $action) = @_;
  1021. if ($action eq 'deviceAdded') {
  1022. print("Device: " . $device->friendlyName . " added.\n");
  1023. }
  1024. elsif ($action eq 'deviceRemoved') {
  1025. print("Device: " . $device->friendlyName . " removed\n");
  1026. }
  1027. }
  1028. =item searchByUDN ( UDN, CALLBACK )
  1029. Used to start a search for devices on the local network by Unique
  1030. Device Name (UDN). Similar to C<searchByType>, this method sends
  1031. out a SSDP C<M-SEARCH> request with a C<ST> header of
  1032. C<upnp:rootdevice>. All responses to the search (and subsequent SSDP
  1033. announcements to the network) are filtered by the C<UDN> parameter
  1034. before resulting in C<CALLBACK> invocation. The parameters to the
  1035. callback are the same as described in C<searchByType>.
  1036. =item searchByFriendlyName ( NAME, CALLBACK )
  1037. Used to start a search for devices on the local network by device
  1038. friendy name. Similar to C<searchByType>, this method sends out a
  1039. SSDP C<M-SEARCH> request with a C<ST> header of
  1040. C<upnp:rootdevice>. All responses to the search (and subsequent SSDP
  1041. announcements to the network) are filtered by the C<NAME> parameter
  1042. before resulting in C<CALLBACK> invocation. The parameters to the
  1043. callback are the same as described in C<searchByType>.
  1044. =item stopSearch ( SEARCH )
  1045. The C<SEARCH> parameter is a
  1046. L<C<UPnP::ControlPoint::Search>|/UPnP::ControlPoint::Search> object
  1047. returned by one of the search methods. This method stops forwarding
  1048. SSDP events that match the search criteria of the specified search.
  1049. =back
  1050. =head2 UPnP::ControlPoint::Device
  1051. A C<UPnP::ControlPoint::Device> is generally obtained using one of the
  1052. L<C<UPnP::ControlPoint>|/UPnP::ControlPoint> search methods and should
  1053. not be directly instantiated.
  1054. =over 4
  1055. =item deviceType
  1056. =item friendlyName
  1057. =item manufacturer
  1058. =item manufacturerURL
  1059. =item modelDescription
  1060. =item modelName
  1061. =item modelNumber
  1062. =item modelURL
  1063. =item serialNumber
  1064. =item UDN
  1065. =item presentationURL
  1066. =item UPC
  1067. Properties received from the device's description document. The
  1068. returned values are all strings.
  1069. =item location
  1070. A URI representing the location of the device on the network.
  1071. =item parent
  1072. The parent device of this device. The value C<undef> if this device
  1073. is a root device.
  1074. =item children
  1075. A list of child devices. The empty list if the device has no
  1076. children.
  1077. =item services
  1078. A list of L<C<UPnP::ControlPoint::Service>|/UPnP::ControlPoint::Service>
  1079. objects corresponding to the services implemented by this device.
  1080. =item getService ( ID )
  1081. If the device implements a service whose serviceType or serviceId is
  1082. equal to the C<ID> parameter, the corresponding
  1083. L<C<UPnP::ControlPoint::Service>|/UPnP::ControlPoint::Service> object
  1084. is returned. Otherwise returns C<undef>.
  1085. =back
  1086. =head2 UPnP::ControlPoint::Service
  1087. A C<UPnP::ControlPoint::Service> is generally obtained from a
  1088. L<C<UPnP::ControlPoint::Device>|/UPnP::ControlPoint::Device> object
  1089. using the C<services> or C<getServiceById> methods. This class should
  1090. not be directly instantiated.
  1091. =over 4
  1092. =item serviceType
  1093. =item serviceId
  1094. =item SCPDURL
  1095. =item controlURL
  1096. =item eventSubURL
  1097. Properties corresponding to the service received from the containing
  1098. device's description document. The returned values are all strings
  1099. except for the URL properties, which are absolute URIs.
  1100. =item actions
  1101. A list of L<C<UPnP::Common::Action>|/UPnP::Common::Action>
  1102. objects corresponding to the actions implemented by this service.
  1103. =item getAction ( NAME )
  1104. Returns the
  1105. L<C<UPnP::Common::Action>|/UPnP::Common::Action> object
  1106. corresponding to the action specified by the C<NAME> parameter.
  1107. Returns C<undef> if no such action exists.
  1108. =item stateVariables
  1109. A list of
  1110. L<C<UPnP::Common::StateVariable>|/UPnP::Common::StateVariable>
  1111. objects corresponding to the state variables implemented by this
  1112. service.
  1113. =item getStateVariable ( NAME )
  1114. Returns the
  1115. L<C<UPnP::Common::StateVariable>|/UPnP::Common::StateVariable>
  1116. object corresponding to the state variable specified by the C<NAME>
  1117. parameter. Returns C<undef> if no such state variable exists.
  1118. =item controlProxy
  1119. Returns a
  1120. L<C<UPnP::ControlPoint::ControlProxy>|/UPnP::ControlPoint::ControlProxy>
  1121. object that can be used to invoke actions on the service.
  1122. =item queryStateVariable ( NAME )
  1123. Generates a SOAP call to the remote service to query the value of the
  1124. state variable specified by C<NAME>. Returns the value of the
  1125. variable. Returns C<undef> if no such state variable exists or the
  1126. variable is not evented.
  1127. =item subscribe ( CALLBACK )
  1128. Registers an event subscription with the remote service. The code
  1129. reference specied by the C<CALLBACK> parameter is invoked when GENA
  1130. events are received from the service. This call returns a
  1131. L<C<UPnP::ControlPoint::Subscription>|/UPnP::ControlPoint::Subscription>
  1132. object corresponding to the subscription. The subscription can later
  1133. be canceled using the C<unsubscribe> method. The parameters to the
  1134. callback are the service object and a list of name-value pairs for all
  1135. of the state variables whose values are included in the corresponding
  1136. GENA event:
  1137. sub eventCallback {
  1138. my ($service, %properties) = @_;
  1139. print("Event received for service " . $service->serviceId . "\n");
  1140. while (my ($key, $val) = each %properties) {
  1141. print("\tProperty ${key}'s value is " . $val . "\n");
  1142. }
  1143. }
  1144. =item unsubscribe ( SUBSCRIPTION )
  1145. Unsubscribe from a service. This method takes the
  1146. L</UPnP::ControlPoint::Subscription>
  1147. object returned from a previous call to C<subscribe>. This method
  1148. is equivalent to calling the C<unsubscribe> method on the subscription
  1149. object itself and is included for symmetry and convenience.
  1150. =back
  1151. =head2 UPnP::Common::Action
  1152. A C<UPnP::Common::Action> is generally obtained from a
  1153. L<C<UPnP::ControlPoint::Service>|/UPnP::ControlPoint::Service> object
  1154. using its C<actions> or C<getAction> methods. It corresponds to an
  1155. action implemented by the service. Action information is retrieved
  1156. from the service's description document. This class should not be
  1157. directly instantiated.
  1158. =over 4
  1159. =item name
  1160. The name of the action returned as a string.
  1161. =item retval
  1162. A L<C<UPnP::Common::Argument>|/UPnP::Common::Argument> object that
  1163. corresponds to the action argument that is specified in the service
  1164. description document as the return value for this action. Returns
  1165. C<undef> if there is no specified return value.
  1166. =item arguments
  1167. A list of L<C<UPnP::Common::Argument>|/UPnP::Common::Argument> objects
  1168. corresponding to the arguments of the action.
  1169. =item inArguments
  1170. A list of L<C<UPnP::Common::Argument>|/UPnP::Common::Argument> objects
  1171. corresponding to the input arguments of the action.
  1172. =item outArguments
  1173. A list of L<C<UPnP::Common::Argument>|/UPnP::Common::Argument> objects
  1174. corresponding to the output arguments of the action.
  1175. =back
  1176. =head2 UPnP::Common::Argument
  1177. A C<UPnP::Common::Argument> is generally obtained from a
  1178. L<C<UPnP::Common::Action>|/UPnP::Common::Action> object using its
  1179. C<arguments>, C<inArguments> or C<outArguments> methods. An instance
  1180. of this class corresponds to an argument of a service action, as
  1181. specified in the service's description document. This class should not
  1182. be directly instantiated.
  1183. =over 4
  1184. =item name
  1185. The name of the argument returned as a string.
  1186. =item relatedStateVariable
  1187. The name of the related state variable (which can be used to find the
  1188. type of the argument) returned as a string.
  1189. =back
  1190. =head2 UPnP::Common::StateVariable
  1191. A C<UPnP::Common::StateVariable> is generally obtained from a
  1192. L<C<UPnP::ControlPoint::Service>|/UPnP::ControlPoint::Service> object
  1193. using its C<stateVariables> or C<getStateVariable> methods. It
  1194. corresponds to a state variable implemented by the service. State
  1195. variable information is retrieved from the service's description
  1196. document. This class should not be directly instantiated.
  1197. =over 4
  1198. =item name
  1199. The name of the state variable returned as a string.
  1200. =item evented
  1201. Whether the state variable is evented or not.
  1202. =item type
  1203. The listed UPnP type of the state variable returned as a string.
  1204. =item SOAPType
  1205. The corresponding SOAP type of the state variable returned as a
  1206. string.
  1207. =back
  1208. =head2 UPnP::ControlPoint::ControlProxy
  1209. A proxy that can be used to invoke actions on a UPnP service. An
  1210. instance of this class is generally obtained from the C<controlProxy>
  1211. method of the corresponding
  1212. L<C<UPnP::ControlPoint::Service>|/UPnP::ControlPoint::Service>
  1213. object. This class should not be directly instantiated.
  1214. An instance of this class is a wrapper on a C<SOAP::Lite> proxy. An
  1215. action is invoked as if it were a method of the proxy
  1216. object. Parameters to the action should be passed to the method. They
  1217. will automatically be coerced to the correct type. For example, to
  1218. invoke the C<Browse> method on a UPnP ContentDirectory service to get
  1219. the children of the root directory, one would say:
  1220. my $proxy = $service->controlProxy;
  1221. my $result = $proxy->Browse('0', 'BrowseDirectChildren', '*', 0, 0, "");
  1222. The result of a action invocation is an instance of the
  1223. L<C<UPnP::ControlPoint::ActionResult>|/UPnP::ControlPoint::ActionResult>
  1224. class.
  1225. =head2 UPnP::ControlPoint::ActionResult
  1226. An instance of this class is returned from an action invocation made
  1227. through a
  1228. L<C<UPnP::ControlPoint::ControlProxy>|/UPnP::ControlPoint::ControlProxy>
  1229. object. It is a loose wrapper on the C<SOAP::SOM> object returned from
  1230. the call made through the C<SOAP::Lite> module. All methods not
  1231. recognized by this class will be forwarded directly to the
  1232. C<SOAP::SOM> class. This class should not be directly instantiated.
  1233. =over 4
  1234. =item isSuccessful
  1235. Was the invocation successful or did it result in a fault.
  1236. =item getValue ( NAME )
  1237. Gets the value of an out argument of the action invocation. The
  1238. C<NAME> parameter specifies which out argument value should be
  1239. returned. The type of the returned value depends on the type
  1240. specified in the service description file.
  1241. =back
  1242. =head2 UPnP::ControlPoint::Search
  1243. A C<UPnP::ControlPoint::Search> object is returned from any successful
  1244. calls to the L<C<UPnP::ControlPoint>|/UPnP::ControlPoint> search
  1245. methods. It has no methods of its own, but can be used as a token to
  1246. pass to any subsequent C<stopSearch> calls. This class should not be
  1247. directly instantiated.
  1248. =head2 UPnP::ControlPoint::Subscription
  1249. A C<UPnP::ControlPoint::Search> object is returned from any successful
  1250. calls to the
  1251. L<C<UPnP::ControlPoint::Service>|/UPnP::ControlPoint::Service>
  1252. C<subscribe> method. This class should not be directly instantiated.
  1253. =over 4
  1254. =item SID
  1255. The subscription ID returned from the remote service, returned as a
  1256. string.
  1257. =item timeout
  1258. The timeout value returned from the remote service, returned as a
  1259. number.
  1260. =item expired
  1261. Has the subscription expired yet?
  1262. =item renew
  1263. Renews a subscription with the remote service by sending a GENA
  1264. subscription event.
  1265. =item unsubscribe
  1266. Unsubscribes from the remote service by sending a GENA unsubscription
  1267. event.
  1268. =back
  1269. =head1 SEE ALSO
  1270. UPnP documentation and resources can be found at L<http://www.upnp.org>.
  1271. The C<SOAP::Lite> module can be found at L<http://www.soaplite.com>.
  1272. UPnP ControlPoint implementations in other languages include the UPnP
  1273. SDK for Linux (L<http://upnp.sourceforge.net/>), Cyberlink for Java
  1274. (L<http://www.cybergarage.org/net/upnp/java/index.html>) and C++
  1275. (L<http://sourceforge.net/projects/clinkcc/>), and the Microsoft UPnP
  1276. SDK
  1277. (L<http://msdn.microsoft.com/library/default.asp?url=/library/en-us/upnp/upnp/universal_plug_and_play_start_page.asp>).
  1278. =head1 AUTHOR
  1279. Vidur Apparao (vidurapparao@users.sourceforge.net)
  1280. =head1 COPYRIGHT AND LICENSE
  1281. Copyright (C) 2004 by Vidur Apparao
  1282. This library is free software; you can redistribute it and/or modify
  1283. it under the same terms as Perl itself, either Perl version 5.8 or,
  1284. at your option, any later version of Perl 5 you may have available.
  1285. =cut
  1286. # Local Variables:
  1287. # tab-width:4
  1288. # indent-tabs-mode:t
  1289. # End: