export tower/sector data to TowerCoverage API, #39776
[freeside.git] / FS / FS / part_export / tower_towercoverage.pm
1 package FS::part_export::tower_towercoverage;
2
3 use strict;
4 use base qw( FS::part_export );
5 use FS::Record qw(qsearch qsearchs dbh);
6 use FS::hardware_class;
7 use FS::hardware_type;
8
9 use vars qw( %options %info
10              %frequency_id %antenna_type_id );
11
12 use Color::Scheme;
13 use LWP::UserAgent;
14 use XML::LibXML::Simple qw(XMLin);
15 use Data::Dumper;
16
17 # note this is not https
18 our $base_url = 'http://api.towercoverage.com/towercoverage.asmx/';
19
20 our $DEBUG = 0;
21 our $me = '[towercoverage.com]';
22
23 sub debug {
24   warn "$me ".join("\n",@_)."\n"
25     if $DEBUG;
26 }
27
28 # hardware class to use for antenna defs
29 my $classname = 'TowerCoverage.com antenna';
30
31 tie %options, 'Tie::IxHash', (
32   'debug'       => { label => 'Enable debugging', type => 'checkbox' },
33
34   'Account'     => { label  => 'Account ID' },
35   'key'         => { label  => 'API key' },
36   'use_coverage'  => { label => 'Enable coverage maps', type => 'checkbox' },
37   'FrequencyID' => { label    => 'Frequency band',
38                      type     => 'select',
39                      options  => [ keys(%frequency_id) ],
40                      option_labels => \%frequency_id,
41                    },
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)',
47                              default => 0,
48                            },
49   '2'           => { type => 'title', label => 'Performance requirements' },
50   'WeakRxThreshold'     => { label => 'Low quality (dBm)', },
51   'StrongRxThreshold'   => { label => 'High quality (dBm)', },
52   'RequiredReliability' => { label => 'Reliability %',
53                              default => 70
54                            },
55 );
56
57 %info = (
58   'svc'     => [qw( tower_sector )],
59   'desc'    => 'TowerCoverage.com coverage mapping and site qualification',
60   'options' => \%options,
61   'no_machine' => 1,
62   'notes'   => <<'END',
63 Export tower/sector configurations to TowerCoverage.com for coverage map
64 generation.
65 END
66 );
67
68 sub insert {
69   my $self = shift;
70
71   my $oldAutoCommit = $FS::UID::AutoCommit;
72   local $FS::UID::AutoCommit = 0;
73
74   my $error = $self->SUPER::insert(@_);
75   return $error if $error;
76
77   my $hwclass = _hardware_class();
78   if (!$hwclass) {
79
80     $hwclass = FS::hardware_class->new({ classname => $classname });
81     $error = $hwclass->insert;
82     if ($error) {
83       dbh->rollback if $oldAutoCommit;
84       return "error creating hardware class for antenna types: $error";
85     }
86
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,
91         model     => $name,
92         title     => $id,
93       });
94       $error = $hardware_type->insert;
95       if ($error) {
96         dbh->rollback if $oldAutoCommit;
97         return "error creating hardware class for antenna types: $error";
98       }
99     }
100   }
101   dbh->commit if $oldAutoCommit;
102   '';
103 }
104
105 sub export_insert {
106   my ($self, $sector) = @_;
107
108   return unless $self->option('use_coverage');
109   local $DEBUG = $self->option('debug') ? 1 : 0;
110
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');
117
118   my $scheme = Color::Scheme->new->from_hex($tower->color || '00FF00');
119
120   my $antenna = qsearchs('hardware_type', {
121     typenum => $sector->hardware_typenum
122   });
123   return "antenna type required" unless $antenna;
124
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.
132   my $data = [
133     'Account'                     => $self->option('Account'),
134     'key'                         => $self->option('key'),
135     'Coverageid'                  => $sector->title,
136     'Coveragename'                => $sector->description,
137     'TowerSiteid'                 => '',
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],
160     'Opacity'                     => 50,
161     'MaximumRange'                => $maximumrange_km,
162     # this could be selectable but there's no reason to do that
163     'RenderingQuality'            => 3,
164     'UseLandCover'                => 1,
165     'UseTwoRays'                  => 1,
166     'CreateViewshed'              => 0,
167   ];
168   debug Dumper($data);
169   $self->http_queue(
170     'action'    => 'insert',
171     'path'      => 'CoverageAPI',
172     'sectornum' => $sector->sectornum,
173     'data'      => $data
174   );
175
176 }
177
178 sub export_replace { # do the same thing as insert
179   my $self = shift;
180   $self->export_insert(@_);
181 }
182
183 sub export_delete { '' }
184
185 =item http_queue
186
187 Queue a job to send an API request.
188 Takes arguments:
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 
193 to which it will add
194 'exportnum' => the exportnum
195
196 =cut
197  
198 sub http_queue {
199   my $self = shift;
200   my $queue = new FS::queue { 'job' => "FS::part_export::tower_towercoverage::http" };
201   return $queue->insert(
202     exportnum => $self->exportnum,
203     @_
204   );
205 }
206
207 sub http {
208   my %params = @_;
209   my $self = FS::part_export->by_key($params{'exportnum'});
210   local $DEBUG = $self->option('debug') ? 1 : 0;
211
212   local $FS::tower_sector::noexport_hack = 1; # avoid recursion
213
214   my $url = $base_url . $params{'path'};
215
216   my $ua = LWP::UserAgent->new;
217
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);
222
223   die $response->error_as_HTML if $response->is_error;
224   debug "received ".$response->decoded_content;
225
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);
233   }
234 }
235
236 sub after_insert {
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;
244   } else {
245     die "$me can't parse map path '$png_path'\n";
246   }
247 }
248
249 sub _hardware_class {
250   qsearchs( 'hardware_class', { classname => $classname });
251 }
252
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';
257
258   foreach my $type (qsearch({
259     table     => 'hardware_type',
260     hashref   => { 'classnum' => $hardware_class->classnum },
261     order_by  => ' order by title::integer'
262   })) {
263     $t{$type->typenum} = $type->model;
264   }
265
266   return \%t;
267 }
268
269 sub export_links {
270   my $self = shift;
271   my ($sector, $arrayref) = @_;
272   if ( $sector->title =~ /^\d+$/ ) {
273     my $link = "http://www.towercoverage.com/En-US/Dashboard/editcoverages/".
274                $sector->title;
275     push @$arrayref, qq!<a href="$link" target="_blank">TowerCoverage map</a>!;
276   }
277 }
278
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
282
283 BEGIN {
284   tie our %frequency_id, 'Tie::IxHash', (
285     1 => "2400 MHz",
286     2 => "5700 MHz",
287     3 => "5300 MHz",
288     4 => "900 MHz",
289     5 => "3650 MHz",
290     12 => "584 MHz",
291     13 => "24000 MHz",
292     14 => "11000 MHz Licensed",
293     15 => "815 MHz",
294     16 => "860 MHz",
295     17 => "1800 MHz CDMA 3G",
296     18 => "18000 MHz Licensed",
297     19 => "1700 MHz",
298     20 => "2100 MHz AWS",
299     21 => "2500-2700 MHz EBS/BRS",
300     22 => "6000 MHz Licensed",
301     23 => "476 MHz",
302     24 => "4900 MHz - Public Safety",
303     25 => "2300 MHz",
304     28 => "7000 MHz 4PSK",
305     29 => "12000 MHz 4PSK",
306     30 => "60 MHz",
307     31 => "260 MHz",
308     32 => "70 MHz",
309     34 => "155 MHz",
310     35 => "365 MHz",
311     36 => "435 MHz",
312     38 => "3500 MHz",
313     39 => "750 MHz",
314     40 => "27 MHz",
315     41 => "10000 MHz",
316     42 => "10250 Mhz",
317     43 => "10250 Mhz",
318     44 => "160 MHz",
319     45 => "700 MHz",
320     46 => "722 MHz",
321     47 => "38000 Mhz",
322     49 => "551 MHz",
323     50 => "600 MHz",
324     51 => "2300 MHz",
325     52 => "5100 MHz",
326     53 => "1900Mhz",
327   );
328
329   # there has to be a better way to handle this. load it during upgrade?
330   # provide a proxy method like get_dids?
331
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&deg; Sector",
343     33 => "PMP450 - 90&deg; 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',
385     78 => 'UBNT - NBM3',
386     79 => 'UBNT - NBM9',
387     80 => 'UBNT - NSM2',
388     81 => 'UBNT - NSM3',
389     82 => 'UBNT - NSM5',
390     83 => 'UBNT - NSM9',
391     84 => 'UBNT - PBM3',
392     85 => 'UBNT - PBM5',
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',
417   );
418 }
419
420 1;