1 package FS::part_export::tower_towercoverage;
4 use base qw( FS::part_export );
5 use FS::Record qw(qsearch qsearchs dbh);
6 use FS::hardware_class;
9 use vars qw( %options %info
10 %frequency_id %antenna_type_id );
14 use XML::LibXML::Simple qw(XMLin);
17 # note this is not https
18 our $base_url = 'http://api.towercoverage.com/towercoverage.asmx/';
21 our $me = '[towercoverage.com]';
24 warn "$me ".join("\n",@_)."\n"
28 # hardware class to use for antenna defs
29 my $classname = 'TowerCoverage.com antenna';
31 tie %options, 'Tie::IxHash', (
32 'debug' => { label => 'Enable debugging', type => 'checkbox' },
34 'Account' => { label => 'Account ID' },
35 'key' => { label => 'API key' },
36 'use_coverage' => { label => 'Enable coverage maps', type => 'checkbox' },
37 'FrequencyID' => { label => 'Frequency band',
39 options => [ keys(%frequency_id) ],
40 option_labels => \%frequency_id,
42 'MaximumRange' => { label => 'Maximum range (miles)', default => '10' },
43 '1' => { type => 'title', label => 'Client equipment' },
44 'ClientAverageAntennaHeight' => { label => 'Typical antenna height (feet)' },
45 'ClientAntennaGain' => { label => 'Antenna gain (dB)' },
46 'RxLineLoss' => { label => 'Line loss (dB)',
49 '2' => { type => 'title', label => 'Performance requirements' },
50 'WeakRxThreshold' => { label => 'Low quality (dBm)', },
51 'StrongRxThreshold' => { label => 'High quality (dBm)', },
52 'RequiredReliability' => { label => 'Reliability %',
58 'svc' => [qw( tower_sector )],
59 'desc' => 'TowerCoverage.com coverage mapping and site qualification',
60 'options' => \%options,
63 Export tower/sector configurations to TowerCoverage.com for coverage map
71 my $oldAutoCommit = $FS::UID::AutoCommit;
72 local $FS::UID::AutoCommit = 0;
74 my $error = $self->SUPER::insert(@_);
75 return $error if $error;
77 my $hwclass = _hardware_class();
80 $hwclass = FS::hardware_class->new({ classname => $classname });
81 $error = $hwclass->insert;
83 dbh->rollback if $oldAutoCommit;
84 return "error creating hardware class for antenna types: $error";
87 foreach my $id (keys %antenna_type_id) {
88 my $name = $antenna_type_id{$id};
89 my $hardware_type = FS::hardware_type->new({
90 classnum => $hwclass->classnum,
94 $error = $hardware_type->insert;
96 dbh->rollback if $oldAutoCommit;
97 return "error creating hardware class for antenna types: $error";
101 dbh->commit if $oldAutoCommit;
106 my ($self, $sector) = @_;
108 return unless $self->option('use_coverage');
109 local $DEBUG = $self->option('debug') ? 1 : 0;
111 my $tower = $sector->tower;
112 my $height_m = sprintf('%.0f', ($sector->height || $tower->height) / 3.28);
113 my $clientheight_m = sprintf('%.0f', $self->option('ClientAverageAntennaHeight') / 3.28);
114 my $maximumrange_km = sprintf('%.0f', $self->option('MaximumRange') * 1.61);
115 my $strongmargin = $self->option('StrongRxThreshold')
116 - $self->option('WeakRxThreshold');
118 my $scheme = Color::Scheme->new->from_hex($tower->color || '00FF00');
120 my $antenna = qsearchs('hardware_type', {
121 typenum => $sector->hardware_typenum
123 return "antenna type required" unless $antenna;
125 # - ALL parameters must be present (or it throws a generic 500 error).
126 # - ONLY Coverageid and TowerSiteid are allowed to be empty.
127 # - ALL parameter names are case sensitive.
128 # - ALL numeric parameters are required to be integers, except for the
129 # coordinates, line loss factors, and center frequency.
130 # - Export options (like RxLineLoss) have a problem where if they're set
131 # to numeric zero, they get removed; make sure we actually send zero.
133 'Account' => $self->option('Account'),
134 'key' => $self->option('key'),
135 'Coverageid' => $sector->title,
136 'Coveragename' => $sector->description,
138 'Latitude' => $tower->latitude,
139 'Longitude' => $tower->longitude,
140 'AntennaHeight' => $height_m,
141 'ClientAverageAntennaHeight' => $clientheight_m,
142 'ClientAntennaGain' => $self->option('ClientAntennaGain'),
143 'RxLineLoss' => sprintf('%.1f', $self->option('RxLineLoss')),
144 'AntennaType' => $antenna->title,
145 'AntennaAzimuth' => int($sector->direction),
146 # note that TowerCoverage bases their coverage map on the antenna
147 # radiation pattern, not on this number.
148 'BeamwidthFilter' => $sector->width,
149 'AntennaTilt' => int($sector->downtilt),
150 'AntennaGain' => int($sector->antenna_gain),
151 'Frequency' => $self->option('FrequencyID'),
152 'ExactCenterFrequency' => $sector->freq_mhz,
153 'TXPower' => int($sector->power),
154 'TxLineLoss' => sprintf('%.1f', $sector->line_loss),
155 'RxThreshold' => $self->option('WeakRxThreshold'),
156 'RequiredReliability' => $self->option('RequiredReliability'),
157 'StrongSignalMargin' => $strongmargin,
158 'StrongSignalColor' => ($scheme->colors)[0],
159 'WeakSignalColor' => ($scheme->colors)[2],
161 'MaximumRange' => $maximumrange_km,
162 # this could be selectable but there's no reason to do that
163 'RenderingQuality' => 3,
166 'CreateViewshed' => 0,
170 'action' => 'insert',
171 'path' => 'CoverageAPI',
172 'sectornum' => $sector->sectornum,
178 sub export_replace { # do the same thing as insert
180 $self->export_insert(@_);
183 sub export_delete { '' }
187 Queue a job to send an API request.
189 'action' => what we're doing (for triggering after_* callback)
190 'path' => the path under TowerCoverage.asmx/
191 'sectornum' => the sectornum
192 'data' => arrayref/hashref of params to send
194 'exportnum' => the exportnum
200 my $queue = new FS::queue { 'job' => "FS::part_export::tower_towercoverage::http" };
201 return $queue->insert(
202 exportnum => $self->exportnum,
209 my $self = FS::part_export->by_key($params{'exportnum'});
210 local $DEBUG = $self->option('debug') ? 1 : 0;
212 local $FS::tower_sector::noexport_hack = 1; # avoid recursion
214 my $url = $base_url . $params{'path'};
216 my $ua = LWP::UserAgent->new;
218 # URL is the same for insert and replace.
219 my $req = HTTP::Request::Common::POST( $url, $params{'data'} );
220 debug("sending $url", $req->content);
221 my $response = $ua->request($req);
223 die $response->error_as_HTML if $response->is_error;
224 debug "received ".$response->decoded_content;
226 # throws exception on parse error
227 my $response_data = XMLin($response->decoded_content);
228 my $method = "after_" . $params{action};
229 if ($self->can($method)) {
230 # should be some kind of event handler, that would be sweet
231 my $sector = FS::tower_sector->by_key($params{'sectornum'});
232 $self->$method($sector, $response_data);
237 my ($self, $sector, $data) = @_;
238 my ($png_path, $kml_path) = split("\n", $data->{content});
239 die "$me no coverage map paths in response\n" unless $png_path;
240 if ( $png_path =~ /(\d+).png$/ ) {
241 $sector->set('title', $1);
242 my $error = $sector->replace;
243 die $error if $error;
245 die "$me can't parse map path '$png_path'\n";
249 sub _hardware_class {
250 qsearchs( 'hardware_class', { classname => $classname });
253 sub get_antenna_types {
254 my $hardware_class = _hardware_class() or return;
255 # return hardware typenums, not TowerCoverage IDs.
256 tie my %t, 'Tie::IxHash';
258 foreach my $type (qsearch({
259 table => 'hardware_type',
260 hashref => { 'classnum' => $hardware_class->classnum },
261 order_by => ' order by title::integer'
263 $t{$type->typenum} = $type->model;
271 my ($sector, $arrayref) = @_;
272 if ( $sector->title =~ /^\d+$/ ) {
273 my $link = "http://www.towercoverage.com/En-US/Dashboard/editcoverages/".
275 push @$arrayref, qq!<a href="$link" target="_blank">TowerCoverage map</a>!;
279 # we can query this from them, but that requires the account id and key...
280 # XXX do some jquery magic in the UI to grab the account ID and key from
281 # those fields, and then look it up right there
284 tie our %frequency_id, 'Tie::IxHash', (
292 14 => "11000 MHz Licensed",
295 17 => "1800 MHz CDMA 3G",
296 18 => "18000 MHz Licensed",
298 20 => "2100 MHz AWS",
299 21 => "2500-2700 MHz EBS/BRS",
300 22 => "6000 MHz Licensed",
302 24 => "4900 MHz - Public Safety",
304 28 => "7000 MHz 4PSK",
305 29 => "12000 MHz 4PSK",
329 # there has to be a better way to handle this. load it during upgrade?
330 # provide a proxy method like get_dids?
332 tie our %antenna_type_id, 'Tie::IxHash', (
333 1 => 'Generic - Omni',
334 5 => 'Generic - 120 Degree',
335 8 => 'Generic - 45 Degree Panel',
336 9 => 'Generic - 60 Degree Panel',
337 10 => 'Generic - 60 Degree x 8 Sectors',
338 11 => 'Generic - 90 Degree',
339 12 => 'Alvarion 3.65 WiMax Base Satation',
340 24 => 'Tranzeo - 3.5 GHz 17db 60 Sector',
341 31 => 'Alpha - 2.3 2033 Omni',
342 32 => "PMP450 - 60° Sector",
343 33 => "PMP450 - 90° Sector",
344 34 => 'PMP450 - SM Panel',
345 36 => 'KPPA - 2GHZDP90S-45 17 dBi',
346 37 => 'KPPA - 2GHZDP120S-45 14.2 dBi',
347 38 => 'KPPA - 3GHZDP60S-45 16.3 dBi',
348 39 => 'KPPA - 3GHZDP90S-45 16.7 dBi',
349 40 => 'KPPA - 3GHZDP120S-45 14.8 dBi',
350 41 => 'KPPA - 5GHZDP40S-17 18.2 dBi',
351 42 => 'KPPA - 5GHZDP60S 17.7 dBi',
352 43 => 'KPPA - 5GHZDP60S-17 18.2 dBi',
353 44 => 'KPPA - 5GHZDP90S 17 dBi',
354 45 => 'KPPA - 5GHZDP120S 16.3 dBi',
355 46 => 'KPPA - OMNI-DP-2 13 dBi',
356 47 => 'KPPA - OMNI-DP-2.4-45 10.7 dBi',
357 48 => 'KPPA - OMNI-DP-3 13 dBi',
358 49 => 'KPPA - OMNI-DP-3-45 11 dBi',
359 51 => 'KPPA - OMNI-DP-5 14 dBi',
360 53 => 'Telrad - 65 Degree 3.65 Ghz',
361 54 => 'KPPA - 2GHZDP60S-17-45 15.1 dBi',
362 55 => 'KPPA - 2GHZDP60S-45 17.9 dBi',
363 56 => 'UBNT - AG-2G20',
364 57 => 'UBNT - AG-5G23',
365 58 => 'UBNT - AG-5G27',
366 59 => 'UBNT - AM-2G15-120',
367 60 => 'UBNT - AM-2G16-90',
368 61 => 'UBNT - AM-3G18-120',
369 62 => 'UBNT - AM-5G16-120',
370 63 => 'UBNT - AM-5G17-90',
371 64 => 'UBNT - AM-5G19-120',
372 65 => 'UBNT - AM-5G20-90',
373 66 => 'UBNT - AM-9G15-90',
374 67 => 'UBNT - AMO-2G10',
375 68 => 'UBNT - AMO-2G13',
376 69 => 'UBNT - AMO-5G10',
377 70 => 'UBNT - AMO-5G13',
378 71 => 'UBNT - AMY-9M16',
379 72 => 'UBNT - LOCOM2',
380 73 => 'UBNT - LOCOM5',
381 74 => 'UBNT - LOCOM9',
382 75 => 'UBNT - NB-2G18',
383 76 => 'UBNT - NB-5G22',
384 77 => 'UBNT - NB-5G25',
393 86 => 'UBNT - PBM10',
394 87 => 'UBNT - RD-2G23',
395 88 => 'UBNT - RD-3G25',
396 89 => 'UBNT - RD-5G30',
397 90 => 'UBNT - RD-5G34',
398 92 => 'TerraWave - 2.3-2.7 18db 65-Degree Panel',
399 93 => 'UBNT - AM-M521-60-AC',
400 94 => 'UBNT - AM-M522-45-AC',
401 101 => 'RF Elements - SH-TP-5-30',
402 104 => 'RF Elements - SH-TP-5-40',
403 105 => 'RF Elements - SH-TP-5-50',
404 106 => 'RF Elements - SH-TP-5-60',
405 107 => 'RF Elements - SH-TP-5-70',
406 108 => 'RF Elements - SH-TP-5-80',
407 109 => 'RF Elements - SH-TP-5-90',
408 110 => 'UBNT - Test',
409 111 => '60 Titanium',
410 112 => '3.65GHz - 6x6',
411 113 => 'AW3015-t0-c4(EOS)',
412 114 => 'AW3035 (EOS)',
413 122 => 'RF Elements - SEC-CC-5-20',
414 135 => 'RF Elements - SEC-CC-2-14',
415 137 => 'RF Elements - SEC-CC-5-17',
416 168 => 'KPPA - Mimosa - 5GHZZHV4P65S-17',