ControlPoint.pm 44 KB

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