RT# 78356 - updated documentation and added ability to create access points as Saisei...
authorChristopher Burger <burgerc@freeside.biz>
Tue, 13 Mar 2018 18:07:39 +0000 (14:07 -0400)
committerChristopher Burger <burgerc@freeside.biz>
Wed, 28 Mar 2018 14:04:27 +0000 (10:04 -0400)
Conflicts:
FS/FS/Schema.pm
FS/FS/tower_sector.pm
httemplate/edit/process/tower.html
httemplate/edit/tower.html
httemplate/elements/tr-tower_sectors.html

FS/FS/Schema.pm
FS/FS/part_export/saisei.pm
FS/FS/tower.pm
FS/FS/tower_sector.pm
httemplate/edit/process/tower.html
httemplate/edit/tower.html
httemplate/elements/tr-tower_sectors.html [new file with mode: 0644]

index 91c91f8..cc74703 100644 (file)
@@ -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' ], ],
index fc0dee5..f76051e 100644 (file)
@@ -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,
       },
   );
 
index f371ec9..5dcf1f8 100644 (file)
@@ -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;
 
index 3fadc86..b58cacf 100644 (file)
@@ -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')
index d14ac56..e17cd55 100644 (file)
@@ -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
                      )],
                    },
 &>
index 377a33e..6607888 100644 (file)
@@ -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 (file)
index 0000000..6843f4f
--- /dev/null
@@ -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 %>">&deg;
+    <label for="<% $id %>_downtilt"><% emt('Down tilt') %></label>
+    <input size="2"
+           id="<% $id %>_downtilt"
+           name="<% $id %>_downtilt"
+           value="<% $sector->downtilt |h %>">&deg;
+  </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">&ndash; </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 %>">&deg;
+    <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 %>">&deg;
+  </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>