diff options
-rw-r--r-- | FS/FS/Schema.pm | 5 | ||||
-rw-r--r-- | FS/FS/part_export/saisei.pm | 190 | ||||
-rw-r--r-- | FS/FS/tower.pm | 10 | ||||
-rw-r--r-- | FS/FS/tower_sector.pm | 14 | ||||
-rw-r--r-- | httemplate/edit/process/tower.html | 2 | ||||
-rw-r--r-- | httemplate/edit/tower.html | 7 | ||||
-rw-r--r-- | httemplate/elements/tr-tower_sectors.html | 310 |
7 files changed, 488 insertions, 50 deletions
diff --git a/FS/FS/Schema.pm b/FS/FS/Schema.pm index 91c91f8..cc74703 100644 --- a/FS/FS/Schema.pm +++ b/FS/FS/Schema.pm @@ -4891,6 +4891,8 @@ sub tables_hashref { 'height', 'decimal', 'NULL', '', '', '', 'veg_height', 'decimal', 'NULL', '', '', '', 'color', 'varchar', 'NULL', 6, '', '', + 'up_rate', 'int', 'NULL', '', '', '', + 'down_rate', 'int', 'NULL', '', '', '', ], 'primary_key' => 'towernum', 'unique' => [ [ 'towername' ] ], # , 'agentnum' ] ], @@ -4916,6 +4918,9 @@ sub tables_hashref { 'east', 'decimal', 'NULL', '10,7', '', '', 'south', 'decimal', 'NULL', '10,7', '', '', 'north', 'decimal', 'NULL', '10,7', '', '', + 'title', 'varchar', 'NULL', $char_d,'', '', + 'up_rate', 'int', 'NULL', '', '', '', + 'down_rate', 'int', 'NULL', '', '', '', ], 'primary_key' => 'sectornum', 'unique' => [ [ 'towernum', 'sectorname' ], [ 'ip_addr' ], ], diff --git a/FS/FS/part_export/saisei.pm b/FS/FS/part_export/saisei.pm index fc0dee5..f76051e 100644 --- a/FS/FS/part_export/saisei.pm +++ b/FS/FS/part_export/saisei.pm @@ -24,18 +24,29 @@ Saisei integration for Freeside This export offers basic svc_broadband provisioning for Saisei. -This is a customer integration with Saisei. This will setup a rate plan and tie -the rate plan to a host via the Saisei API when the broadband service is provisioned. -It will also untie the rate plan via the API upon unprovisioning of the broadband service. +This is a customer integration with Saisei. This will setup a rate plan and tie +the rate plan to a host and access point via the Saisei API when the broadband service is provisioned. +It will also untie the rate plan via the API upon unprovisioning of the broadband service. -This export will use the broadband service descriptive label for the Saisei rate plan name and -will use the email from the first contact for the Saisei username that will be -attached to this rate plan. It will use the Saisei default Access Point. +Add a new export and fill out required fields: +<UL> +<LI>Hostname or IP - <I>Host name to Saisei API</I></LI> +<LI>Port - <I>Port number to Saisei API</I></LI> +<LI>User Name - <I>Saisei API user name</I></LI> +<LI>Password - <I>Saisei API password</I></LI> +</UL> +Create a broadband service. The broadband service name will become the Saisei rate plan name. +Set the upload and download speed, and set the modifier to fixed. +Set IP Address to required. +Attach Saisei export to service + +Create a tower and add a sector to that tower. The sector name will be the name of the access point, +Make sure you have set an up and down rate for the Tower and Sector. + +When you provision the service, enter the ip address associated to this service. +Select the Tower and Sector for it's access point. -Hostname or IP - Host name to Saisei API -Port - <I>Port number to Saisei API -User Name - <I>Saisei API user name -Password - <I>Saisei API password +When the service is provisioned it will auto setup the rate plan. This module also provides generic methods for working through the L</Saisei API>. @@ -58,27 +69,37 @@ tie my %options, 'Tie::IxHash', 'options' => \%options, 'notes' => <<'END', This is a customer integration with Saisei. This will setup a rate plan and tie -the rate plan to a host via the Saisei API when the broadband service is provisioned. -It will also untie the rate plan via the API upon unprovisioning of the broadband service. -<P>This export will use the broadband service descriptive label for the Saisei rate plan name and -will use the email from the first contact for the Saisei username that will be -attached to this rate plan. It will use the Saisei default Access Point. +the rate plan to a host and access point via the Saisei API when the broadband service is provisioned. +It will also untie the rate plan via the API upon unprovisioning of the broadband service. <P> -Required Fields: +Add a new export and fill out required fields: <UL> <LI>Hostname or IP - <I>Host name to Saisei API</I></LI> <LI>Port - <I>Port number to Saisei API</I></LI> <LI>User Name - <I>Saisei API user name</I></LI> <LI>Password - <I>Saisei API password</I></LI> </UL> +Create a broadband service. The broadband service name will become the Saisei rate plan name. +Set the upload and download speed, and set the modifier to fixed. +Set IP Address to required. +Attach Saisei export to service +<P> +Create a tower and add a sector to that tower. The sector name will be the name of the access point, +Make sure you have set an up and down rate for the Tower and Sector. +<P> +When you provision the service, enter the ip address associated to this service. +Select the Tower and Sector for it's access point. +<P> +When the service is provisioned it will auto setup the rate plan. END ); sub _export_insert { my ($self, $svc_broadband) = @_; - my $rateplan_name = $svc_broadband->{Hash}->{description}; - $rateplan_name =~ s/\s/_/g; + my $service_part = FS::Record::qsearchs( 'part_svc', { 'svcpart' => $svc_broadband->{Hash}->{svcpart} } ); + my $rateplan_name = $service_part->{Hash}->{svc}; + $rateplan_name =~ s/\s/_/g; # load needed info from our end my $cust_main = $svc_broadband->cust_main; @@ -99,19 +120,13 @@ sub _export_insert { # set rateplan to existing one or newly created one. my $rateplan = $existing_rateplan ? $existing_rateplan : $self->api_get_rateplan($rateplan_name); - my @email = map { $_->emailaddress } FS::Record::qsearch({ - 'table' => 'cust_contact', - 'select' => 'emailaddress', - 'addl_from' => ' JOIN contact_email USING (contactnum)', - 'hashref' => { 'custnum' => $cust_main->{Hash}->{custnum}, }, - }); - my $username = $email[0]; - my $description = $cust_main->{Hash}->{first}." ".$cust_main->{Hash}->{last}; + my $username = $svc_broadband->{Hash}->{svcnum}; + my $description = $svc_broadband->{Hash}->{description}; if (!$username) { $self->{'__saisei_error'} = 'no username - can not export'; - warn "No email found $username\n" if $self->option('debug'); - return; + warn "No user $username\n" if $self->option('debug'); + return $self->api_error; } else { # check for existing user. @@ -125,12 +140,65 @@ sub _export_insert { my $user = $existing_user ? $existing_user : $self->api_get_user($username); ## add access point ? - - ## tie host to user - $self->api_add_host_to_user($user->{collection}->[0]->{name}, $rateplan->{collection}->[0]->{name}, $svc_broadband->{Hash}->{ip_addr}) unless $self->{'__saisei_error'}; + my $tower_sector = FS::Record::qsearchs({ + 'table' => 'tower_sector', + 'select' => 'tower.towername, + tower.up_rate as toweruprate, + tower.down_rate as towerdownrate, + tower_sector.sectorname, + tower_sector.up_rate as sectoruprate, + tower_sector.down_rate as sectordownrate ', + 'addl_from' => 'LEFT JOIN tower USING ( towernum )', + 'hashref' => { + 'sectornum' => $svc_broadband->{Hash}->{sectornum}, + }, + }); + + my $existing_tower_ap; + my $tower_name = $tower_sector->{Hash}->{towername}; + $tower_name =~ s/\s/_/g; + + #check if tower has been set up as an access point. + $existing_tower_ap = $self->api_get_accesspoint($tower_name) unless $self->{'__saisei_error'};; + + #if tower does not exist as an access point create it. + $self->api_create_accesspoint( + $tower_name, + $tower_sector->{Hash}->{toweruprate}, + $tower_sector->{Hash}->{towerdownrate} + ) unless $existing_tower_ap; + + my $existing_sector_ap; + my $sector_name = $tower_sector->{Hash}->{sectorname}; + $sector_name =~ s/\s/_/g; + + #check if sector has been set up as an access point. + $existing_sector_ap = $self->api_get_accesspoint($sector_name); + + #if sector does not exist as an access point create it. + $self->api_create_accesspoint( + $sector_name, + $tower_sector->{Hash}->{sectoruprate}, + $tower_sector->{Hash}->{sectordownrate}, + $tower_name, + ) unless $existing_sector_ap; + + # Attach newly created sector to it's tower. + $self->api_modify_accesspoint($sector_name, $tower_name) unless ($self->{'__saisei_error'} || $existing_sector_ap); + + # set access point to existing one or newly created one. + my $accesspoint = $existing_sector_ap ? $existing_sector_ap : $self->api_get_accesspoint($sector_name); + + ## tie host to user add sector name as access point. + $self->api_add_host_to_user( + $user->{collection}->[0]->{name}, + $rateplan->{collection}->[0]->{name}, + $svc_broadband->{Hash}->{ip_addr}, + $accesspoint->{collection}->[0]->{name}, + ) unless $self->{'__saisei_error'}; } - return ''; + return $self->api_error; } @@ -229,7 +297,7 @@ sub api_call { =head2 api_error -Returns the error string set by L</PortaOne API> methods, +Returns the error string set by L</Saisei API> methods, or a blank string if most recent call produced no errors. =cut @@ -300,14 +368,14 @@ Gets user info for specific access point. sub api_get_accesspoint { my $self = shift; - my $accesspoint; + my $accesspoint = shift; my $get_accesspoint = $self->api_call("GET", "/access_points/$accesspoint"); return if $self->api_error; - $self->{'__saisei_error'} = "Did not receive any user info" + $self->{'__saisei_error'} = "Did not receive any access point info" unless $get_accesspoint; - return; + return $get_accesspoint; } =head2 api_create_rateplan @@ -397,19 +465,44 @@ Creates a access point. =cut sub api_create_accesspoint { - my ($self,$accesspoint) = @_; + my ($self,$accesspoint, $uprate, $downrate) = @_; # this has not been tested, but should work, if needed. - #my $new_accesspoint = $self->api_call( - # "PUT", - # "/access_points/$accesspoint", - # { - # 'description' => 'my description', - # }, - #); - - #$self->{'__saisei_error'} = "Access point not created" - # unless $new_accesspoint; # should never happen + my $new_accesspoint = $self->api_call( + "PUT", + "/access_points/$accesspoint", + { + 'downstream_rate_limit' => $downrate, + 'upstream_rate_limit' => $uprate, + }, + ); + + $self->{'__saisei_error'} = "Access point not created" + unless $new_accesspoint; # should never happen + return; + +} + +=head2 api_modify_accesspoint + +Modify a access point. + +=cut + +sub api_modify_accesspoint { + my ($self, $accesspoint, $uplink) = @_; + + my $modified_rateplan = $self->api_call( + "PUT", + "/access_points/$accesspoint", + { + 'uplink' => $uplink, # name of attached access point + }, + ); + + $self->{'__saisei_error'} = "Rate Plan not modified" + unless $modified_rateplan; # should never happen + return; } @@ -421,7 +514,7 @@ ties host to user, rateplan and default access point. =cut sub api_add_host_to_user { - my ($self,$user, $rateplan, $ip) = @_; + my ($self,$user, $rateplan, $ip, $accesspoint) = @_; my $new_host = $self->api_call( "PUT", @@ -429,6 +522,7 @@ sub api_add_host_to_user { { 'user' => $user, 'rate_plan' => $rateplan, + 'access_point' => $accesspoint, }, ); diff --git a/FS/FS/tower.pm b/FS/FS/tower.pm index f371ec9..5dcf1f8 100644 --- a/FS/FS/tower.pm +++ b/FS/FS/tower.pm @@ -44,6 +44,14 @@ Tower name Disabled flag, empty or 'Y' +=item up_rate + +Up Rate for towner + +=item down_rate + +Down Rate for tower + =back =head1 METHODS @@ -97,6 +105,8 @@ sub check { || $self->ut_floatn('height') || $self->ut_floatn('veg_height') || $self->ut_alphan('color') + || $self->ut_numbern('up_rate') + || $self->ut_numbern('down_rate') ; return $error if $error; diff --git a/FS/FS/tower_sector.pm b/FS/FS/tower_sector.pm index 3fadc86..b58cacf 100644 --- a/FS/FS/tower_sector.pm +++ b/FS/FS/tower_sector.pm @@ -88,6 +88,18 @@ The coverage map, as a PNG. The coordinate boundaries of the coverage map. +=item title + +The sector title. + +=item up_rate + +Up rate for sector. + +=item down_rate + +down rate for sector. + =back =head1 METHODS @@ -150,6 +162,8 @@ sub check { || $self->ut_numbern('downtilt') || $self->ut_floatn('sector_range') || $self->ut_numbern('margin') + || $self->ut_numbern('up_rate') + || $self->ut_numbern('down_rate') || $self->ut_anything('image') || $self->ut_sfloatn('west') || $self->ut_sfloatn('east') diff --git a/httemplate/edit/process/tower.html b/httemplate/edit/process/tower.html index d14ac56..e17cd55 100644 --- a/httemplate/edit/process/tower.html +++ b/httemplate/edit/process/tower.html @@ -5,7 +5,7 @@ 'fields' => [qw( sectorname ip_addr height freq_mhz direction width downtilt v_width margin - sector_range + sector_range up_rate down_rate )], }, &> diff --git a/httemplate/edit/tower.html b/httemplate/edit/tower.html index 377a33e..6607888 100644 --- a/httemplate/edit/tower.html +++ b/httemplate/edit/tower.html @@ -12,6 +12,8 @@ 'altitude', 'height', 'veg_height', + 'up_rate', + 'down_rate', { field => 'sectornum', type => 'tower_sector', o2m_table => 'tower_sector', @@ -30,6 +32,8 @@ 'height' => 'Height (feet)', 'veg_height' => 'Vegetation height (feet)', 'color' => 'Color', + 'up_rate' => 'Up Rate (Kbps)', + 'down_rate' => 'Down Rate (Kbps)', }, &> <%init> @@ -38,7 +42,8 @@ my $m2_error_callback = sub { # reconstruct the list my ($cgi, $object) = @_; my @fields = qw( - sectorname ip_addr height freq_mhz direction width tilt v_width margin sector_range + sectorname ip_addr height freq_mhz direction width tilt v_width margin + sector_range up_rate down_rate ); map { diff --git a/httemplate/elements/tr-tower_sectors.html b/httemplate/elements/tr-tower_sectors.html new file mode 100644 index 0000000..6843f4f --- /dev/null +++ b/httemplate/elements/tr-tower_sectors.html @@ -0,0 +1,310 @@ +<%shared> +# kind of a hack... +my ($export) = FS::tower_sector->part_export; +my $antenna_types; # will be an ordered hash +if ($export and $export->can('get_antenna_types')) { + $antenna_types = $export->get_antenna_types; +} +</%shared> +<%init> +my %opt = @_; +my $tower = $opt{'object'}; +my $towernum = $tower->towernum; +my $cgi = $opt{'cgi'}; + +my $tabcounter = 0; + +my @fields = qw( + sectorname ip_addr height freq_mhz direction width downtilt v_width + db_high db_low sector_range + power line_loss antenna_gain hardware_typenum up_rate down_rate +); + +my @sectors; +if ( $cgi->param('error') ) { + foreach my $k ($cgi->param) { + if ($k =~ /^sectornum\d+$/) { + my $sectornum = $cgi->param($k); + my $sector = FS::tower_sector->new({ + 'sectornum' => $sectornum, + 'towernum' => $towernum, + map { $_ => scalar($cgi->param($k.'_'.$_)) } @fields, + }); + push @sectors, $sector if length($sector->sectorname); + } + } +} elsif ( $towernum ) { + @sectors = $tower->tower_sector; +} # else new mode, no sectors yet + +my $id = $opt{id} || $opt{field} || 'sectornum'; + +</%init> +<& tablebreak-tr-title.html, value => 'Sectors' &> + +<style> + .ui-tabs-nav a { + padding: 6px 9px; + font-weight: bold; + } + .ui-tabs-nav li { + border-top-left-radius: 0.5em; + border-top-right-radius: 0.5em; + } + .ui-tabs-active li { + border-bottom-color: #fff; + } + .ui-tabs { + font-weight: bold; + } + .ui-tabs label { + padding-top: 3px; + width: 140px; + display: inline-block; + text-align: right; + } + .ui-tabs input, .ui-spinner { + border: 1px solid #666; + border-radius: 2px; + font-size: 13.3px; + text-align: right; + font-weight: normal; + padding: 1px; + } + .ui-tabs input { /* but not spinner, messes it up */ + margin-left: 1px; + margin-right: 1px; + } + .ui-tabs input:focus { + border-color: #7e0079; + background-color: #ffffdd; + } + .ui-spinner input { /* use the spinner's border and padding */ + border: none; + text-align: left; + } + .ui-tabs p { + margin-top: 8px; + margin-bottom: 8px; + } + +</style> + + +<tr> + <td colspan=2> +%# prototypes + <div style="display: none"> +<& .tab, id => $id . '_P' &> +<& .panel, id => $id . '_P' &> + </div> + +%# main container + <div id="<% $id %>_tabs"> + <ul> +% foreach my $sector (@sectors) { +<& .tab, sector => $sector, id => $id . $tabcounter &> +% $tabcounter++; +% } + </ul> + +% $tabcounter = 0; +% foreach my $sector (@sectors) { +<& .panel, sector => $sector, id => $id . $tabcounter &> +% $tabcounter++; +% } + </div> + </td> +</tr> +<script> +$(function() { + var tabcounter = <% $tabcounter %>; + var id = <% $id |js_string %>; + //create tab bar + var tabs = $( '#'+id+'_tabs' ).tabs(); + + function changedSectorName() { + var this_panel = $(this).closest('div'); + var this_tab = tabs.find('#' + this_panel.prop('id') + '_tab'); + // if this is the last panel, make a new one + if (this_panel.next().length == 0) { + addSector(); + } + // and update the current tab's text with the sector name + this_tab.find('a').text($(this).val()); + } + + var tab_proto = $('#'+id+'_P_tab'); + var panel_proto = $('#'+id+'_P'); + + function addSector() { + var new_tab = tab_proto.clone(); + var new_panel = panel_proto.clone(); + // replace proto placeholder with the counter value, in all id and + // name properties in new_panel and its children + new_panel.add( new_panel.find('*') ).each(function() { + this.id = this.id.replace('_P', tabcounter); + if (this.name) { + this.name = this.name.replace('_P', tabcounter); + } + }); + tabcounter++; + // and set the handler up on it + new_panel.find('.input-sectorname').on('change', changedSectorName); + + // also update the tab itself + new_tab.find('a').prop('href', '#' + new_panel.prop('id')); + new_tab.prop('id', new_panel.prop('id') + '_tab'); + + tabs.append(new_panel); + tabs.children('ul:first').append(new_tab); + + tabs.tabs('refresh'); + } + + $('.dbspinner').spinner({ step: 5 }); + + $('.input-sectorname').on('change', changedSectorName); + addSector(); + +}); +</script> +<%def .tab> +% my %opt = @_; +% my $sector = $opt{sector}; +% my $id = $opt{id}; +% my $title = $sector ? $sector->sectorname : mt('Add new'); + <li id="<% $id %>_tab"> + <a href="#<% $id %>"><% $title |h %></a> + </li> +</%def> +<%def .panel> +% my %opt = @_; +% my $sector = $opt{sector} || FS::tower_sector->new({}); +% my $id = $opt{id}; # sectornumX +<div id="<% $id %>"> +% # no id on this one, the panel gets the "sectornumX" id + <input type="hidden" name="<% $id %>" value="<% $sector->sectornum |h %>"> + <p> + <label><% emt('Sector name') %></label> + <input style="text-align: left" + class="input-sectorname" + id="<% $id %>_sectorname" + name="<% $id %>_sectorname" + value="<% $sector->sectorname |h %>"> + + <label><% emt('IP address') %></label> + <input style="text-align: left" + id="<% $id %>_ip_addr" + name="<% $id %>_ip_addr" + value="<% $sector->ip_addr |h %>"> + </p> + <p> + <label for="<% $id %>_height"><% emt('Antenna height') %></label> + <input size="3" + id="<% $id %>_height" + name="<% $id %>_height" + value="<% $sector->height |h %>"> + <% emt('feet above ground') %> + </p> + <p> + <label for="<% $id %>_direction"><% emt('Azimuth') %></label> + <input size="3" + id="<% $id %>_direction" + name="<% $id %>_direction" + value="<% $sector->direction |h %>">° + <label for="<% $id %>_downtilt"><% emt('Down tilt') %></label> + <input size="2" + id="<% $id %>_downtilt" + name="<% $id %>_downtilt" + value="<% $sector->downtilt |h %>">° + </p> + + <p> + <label for="<% $id %>_freq_mhz"><% emt('Frequency') %></label> + <input size="4" + id="<% $id %>_freq_mhz" + name="<% $id %>_freq_mhz" + value="<% $sector->freq_mhz |h %>"> + <% emt('MHz') %> + </p> + + <p> + <label for="<% $id %>_power"><% emt('Transmit power') %></label> + <input size="3" + id="<% $id %>_power" + name="<% $id %>_power" + value="<% $sector->power |h %>"> + <% emt('dBm') %><br> + <label for="<% $id %>_antenna_gain">+ </label> + <input size="3" + id="<% $id %>_antenna_gain" + name="<% $id %>_antenna_gain" + value="<% $sector->antenna_gain |h %>"> + <% emt('dB antenna gain') %><br> + <label for="<% $id %>_line_loss">– </label> + <input size="3" + id="<% $id %>_line_loss" + name="<% $id %>_line_loss" + value="<% $sector->line_loss |h %>"> + <% emt('dB line loss') %> + +% if ( $antenna_types ) { + <p> + <label for="<% $id %>_hardware_typenum"><% emt('Antenna type') %></label> + <& /elements/select.html, + field => $id.'_hardware_typenum', + options => [ '', keys %$antenna_types ], + labels => $antenna_types, + curr_value => $sector->hardware_typenum, + &> + </p> +% } +% # this next section might not be necessary if you enter an antenna type + <p> + <label for="<% $id %>_width"><% emt('Horizontal beam') %></label> + <input size="3" + id="<% $id %>_width" + name="<% $id %>_width" + value="<% $sector->width |h %>">° + <label for="<% $id %>_v_width"><% emt('Vertical beam') %></label> + <input size="2" + id="<% $id %>_v_width" + name="<% $id %>_v_width" + value="<% $sector->v_width |h %>">° + </p> + + <label><% emt('Signal margin') %></label> + <div style="display: inline-block; vertical-align: top"> + <input class="dbspinner" + size="4" + id="<% $id %>_db_high" + name="<% $id %>_db_high" + value="<% $sector->db_high |h %>"> + <% emt('dB (high quality)') %> + <br> + + <input class="dbspinner" + size="4" + id="<% $id %>_db_low" + name="<% $id %>_db_low" + value="<% $sector->db_low |h %>"> + <% emt('dB (low quality)') %> + </div> + <p> + <label><% emt('Up Rate (Kbps)') %></label> + <input style="text-align: left" + id="<% $id %>_up_rate" + name="<% $id %>_up_rate" + value="<% $sector->up_rate |h %>"> + </p> + <p> + <label><% emt('Down Rate (Kbps)') %></label> + <input style="text-align: left" + id="<% $id %>_down_rate" + name="<% $id %>_down_rate" + value="<% $sector->down_rate |h %>"> + </p> + +</div> +</%def> |