Bladeren bron

Merge pull request #290 from sidoh/fut091_support

Add support for FUT091 dual-white remote
Chris Mullins 7 jaren geleden
bovenliggende
commit
d175ba3088

File diff suppressed because it is too large
+ 2 - 2
dist/index.html.gz.h


+ 57 - 0
lib/MiLight/FUT091PacketFormatter.cpp

@@ -0,0 +1,57 @@
+#include <FUT091PacketFormatter.h>
+#include <V2RFEncoding.h>
+#include <Units.h>
+
+static const uint8_t BRIGHTNESS_SCALE_MAX = 0x97;
+static const uint8_t KELVIN_SCALE_MAX = 0xC5;
+
+void FUT091PacketFormatter::updateBrightness(uint8_t value) {
+  command(static_cast<uint8_t>(FUT091Command::BRIGHTNESS), V2PacketFormatter::tov2scale(value, BRIGHTNESS_SCALE_MAX, 2));
+}
+
+void FUT091PacketFormatter::updateTemperature(uint8_t value) {
+  command(static_cast<uint8_t>(FUT091Command::KELVIN), V2PacketFormatter::tov2scale(value, KELVIN_SCALE_MAX, 2, false));
+}
+
+void FUT091PacketFormatter::enableNightMode() {
+  uint8_t arg = groupCommandArg(OFF, groupId);
+  command(static_cast<uint8_t>(FUT091Command::ON_OFF) | 0x80, arg);
+}
+
+BulbId FUT091PacketFormatter::parsePacket(const uint8_t *packet, JsonObject& result) {
+  uint8_t packetCopy[V2_PACKET_LEN];
+  memcpy(packetCopy, packet, V2_PACKET_LEN);
+  V2RFEncoding::decodeV2Packet(packetCopy);
+
+  BulbId bulbId(
+    (packetCopy[2] << 8) | packetCopy[3],
+    packetCopy[7],
+    REMOTE_TYPE_FUT091
+  );
+
+  uint8_t command = (packetCopy[V2_COMMAND_INDEX] & 0x7F);
+  uint8_t arg = packetCopy[V2_ARGUMENT_INDEX];
+
+  if (command == (uint8_t)FUT091Command::ON_OFF) {
+    if ((packetCopy[V2_COMMAND_INDEX] & 0x80) == 0x80) {
+      result["command"] = "night_mode";
+    } else if (arg < 5) { // Group is not reliably encoded in group byte. Extract from arg byte
+      result["state"] = "ON";
+      bulbId.groupId = arg;
+    } else {
+      result["state"] = "OFF";
+      bulbId.groupId = arg-5;
+    }
+  } else if (command == (uint8_t)FUT091Command::BRIGHTNESS) {
+    uint8_t level = V2PacketFormatter::fromv2scale(arg, BRIGHTNESS_SCALE_MAX, 2, true);
+    result["brightness"] = Units::rescale<uint8_t, uint8_t>(level, 255, 100);
+  } else if (command == (uint8_t)FUT091Command::KELVIN) {
+    uint8_t kelvin = V2PacketFormatter::fromv2scale(arg, KELVIN_SCALE_MAX, 2, false);
+    result["color_temp"] = Units::whiteValToMireds(kelvin, 100);
+  } else {
+    result["button_id"] = command;
+    result["argument"] = arg;
+  }
+
+  return bulbId;
+}

+ 25 - 0
lib/MiLight/FUT091PacketFormatter.h

@@ -0,0 +1,25 @@
+#include <V2PacketFormatter.h>
+
+#ifndef _FUT091_PACKET_FORMATTER_H
+#define _FUT091_PACKET_FORMATTER_H
+
+enum class FUT091Command {
+  ON_OFF = 0x01,
+  BRIGHTNESS = 0x2,
+  KELVIN = 0x03
+};
+
+class FUT091PacketFormatter : public V2PacketFormatter {
+public:
+  FUT091PacketFormatter()
+    : V2PacketFormatter(0x21, 4)    // protocol is 0x21, and there are 4 groups
+  { }
+
+  virtual void updateBrightness(uint8_t value);
+  virtual void updateTemperature(uint8_t value);
+  virtual void enableNightMode();
+
+  virtual BulbId parsePacket(const uint8_t* packet, JsonObject& result);
+};
+
+#endif

+ 18 - 5
lib/MiLight/MiLightRemoteConfig.cpp

@@ -5,10 +5,11 @@
  */
 const MiLightRemoteConfig* MiLightRemoteConfig::ALL_REMOTES[] = {
   &FUT096Config, // rgbw
-  &FUT091Config, // cct
+  &FUT007Config, // cct
   &FUT092Config, // rgb+cct
   &FUT098Config, // rgb
-  &FUT089Config  // 8-group rgb+cct (b8, fut089)
+  &FUT089Config, // 8-group rgb+cct (b8, fut089)
+  &FUT091Config
 };
 
 const MiLightRemoteConfig* MiLightRemoteConfig::fromType(const String& type) {
@@ -16,8 +17,8 @@ const MiLightRemoteConfig* MiLightRemoteConfig::fromType(const String& type) {
     return &FUT096Config;
   }
 
-  if (type.equalsIgnoreCase("cct") || type.equalsIgnoreCase("fut091")) {
-    return &FUT091Config;
+  if (type.equalsIgnoreCase("cct") || type.equalsIgnoreCase("fut007")) {
+    return &FUT007Config;
   }
 
   if (type.equalsIgnoreCase("rgb_cct") || type.equalsIgnoreCase("fut092")) {
@@ -32,6 +33,10 @@ const MiLightRemoteConfig* MiLightRemoteConfig::fromType(const String& type) {
     return &FUT098Config;
   }
 
+  if (type.equalsIgnoreCase("v2_cct") || type.equalsIgnoreCase("fut091")) {
+    return &FUT091Config;
+  }
+
   Serial.print(F("MiLightRemoteConfig::fromType: ERROR - tried to fetch remote config for type: "));
   Serial.println(type);
 
@@ -77,7 +82,7 @@ const MiLightRemoteConfig FUT096Config( //rgbw
   4
 );
 
-const MiLightRemoteConfig FUT091Config( //cct
+const MiLightRemoteConfig FUT007Config( //cct
   new CctPacketFormatter(),
   MiLightRadioConfig::ALL_CONFIGS[1],
   REMOTE_TYPE_CCT,
@@ -85,6 +90,14 @@ const MiLightRemoteConfig FUT091Config( //cct
   4
 );
 
+const MiLightRemoteConfig FUT091Config( //v2 cct
+  new FUT091PacketFormatter(),
+  MiLightRadioConfig::ALL_CONFIGS[2],
+  REMOTE_TYPE_FUT091,
+  "fut091",
+  4
+);
+
 const MiLightRemoteConfig FUT092Config( //rgb+cct
   new RgbCctPacketFormatter(),
   MiLightRadioConfig::ALL_CONFIGS[2],

+ 4 - 2
lib/MiLight/MiLightRemoteConfig.h

@@ -6,6 +6,7 @@
 #include <RgbCctPacketFormatter.h>
 #include <CctPacketFormatter.h>
 #include <FUT089PacketFormatter.h>
+#include <FUT091PacketFormatter.h>
 #include <PacketFormatter.h>
 
 #ifndef _MILIGHT_REMOTE_CONFIG_H
@@ -36,14 +37,15 @@ public:
   static const MiLightRemoteConfig* fromType(const String& type);
   static const MiLightRemoteConfig* fromReceivedPacket(const MiLightRadioConfig& radioConfig, const uint8_t* packet, const size_t len);
 
-  static const size_t NUM_REMOTES = 5;
+  static const size_t NUM_REMOTES = 6;
   static const MiLightRemoteConfig* ALL_REMOTES[NUM_REMOTES];
 };
 
 extern const MiLightRemoteConfig FUT096Config; //rgbw
-extern const MiLightRemoteConfig FUT091Config; //cct
+extern const MiLightRemoteConfig FUT007Config; //cct
 extern const MiLightRemoteConfig FUT092Config; //rgb+cct
 extern const MiLightRemoteConfig FUT089Config; //rgb+cct B8 / FUT089
 extern const MiLightRemoteConfig FUT098Config; //rgb
+extern const MiLightRemoteConfig FUT091Config; //v2 cct
 
 #endif

+ 3 - 20
lib/MiLight/RgbCctPacketFormatter.cpp

@@ -40,11 +40,7 @@ void RgbCctPacketFormatter::updateColorRaw(uint8_t value) {
 void RgbCctPacketFormatter::updateTemperature(uint8_t value) {
   // Packet scale is [0x94, 0x92, .. 0, .., 0xCE, 0xCC]. Increments of 2.
   // From coolest to warmest.
-  // To convert from [0, 100] scale:
-  //   * Multiply by 2
-  //   * Reverse direction (increasing values should be cool -> warm)
-  //   * Start scale at 0xCC
-  uint8_t cmdValue = ((100 - value) * 2) + RGB_CCT_KELVIN_REMOTE_END;
+  uint8_t cmdValue = V2PacketFormatter::tov2scale(value, RGB_CCT_KELVIN_REMOTE_END, 2);
 
   // when updating temperature, the bulb switches to white.  If we are not already
   // in white mode, that makes changing temperature annoying because the current hue/mode
@@ -87,7 +83,7 @@ void RgbCctPacketFormatter::updateColorWhite() {
   // there is no direct white command, so let's look up our prior temperature and set that, which
   // causes the bulb to go white 
   GroupState ourState = this->stateStore->get(this->deviceId, this->groupId, REMOTE_TYPE_RGB_CCT);
-  uint8_t value = ((100 - ourState.getKelvin()) * 2) + RGB_CCT_KELVIN_REMOTE_END;
+  uint8_t value = V2PacketFormatter::tov2scale(ourState.getKelvin(), RGB_CCT_KELVIN_REMOTE_END, 2);
 
   // issue command to set kelvin to prior value, which will drive to white
   command(RGB_CCT_KELVIN, value);
@@ -131,20 +127,7 @@ BulbId RgbCctPacketFormatter::parsePacket(const uint8_t *packet, JsonObject& res
     uint16_t hue = Units::rescale<uint16_t, uint16_t>(rescaledColor, 360, 255.0);
     result["hue"] = hue;
   } else if (command == RGB_CCT_KELVIN) {
-    // Packet range is [0x94, 0x92, ..., 0xCC]. Remote sends values outside this
-    // range, so normalize.
-    uint8_t temperature = arg;
-    if (arg < 0xCC && arg >= 0xB0) {
-      temperature = 0xCC;
-    } else if (arg > 0x94 && arg <= 0xAF) {
-      temperature = 0x94;
-    }
-
-    temperature = (temperature + (0x100 - RGB_CCT_KELVIN_REMOTE_END)) % 0x100;
-    temperature /= 2;
-    temperature = (100 - temperature);
-    temperature = constrain(temperature, 0, 100);
-
+    uint8_t temperature = V2PacketFormatter::fromv2scale(arg, RGB_CCT_KELVIN_REMOTE_END, 2);
     result["color_temp"] = Units::whiteValToMireds(temperature, 100);
   // brightness == saturation
   } else if (command == RGB_CCT_BRIGHTNESS && arg >= (RGB_CCT_BRIGHTNESS_OFFSET - 15)) {

+ 25 - 0
lib/MiLight/V2PacketFormatter.cpp

@@ -103,3 +103,28 @@ void V2PacketFormatter::switchMode(GroupState currentState, BulbMode desiredMode
   }
   
 }
+
+uint8_t V2PacketFormatter::tov2scale(uint8_t value, uint8_t endValue, uint8_t interval, bool reverse) {
+  if (reverse) {
+    value = 100 - value;
+  }
+
+  return (value * interval) + endValue;
+}
+
+uint8_t V2PacketFormatter::fromv2scale(uint8_t value, uint8_t endValue, uint8_t interval, bool reverse, uint8_t buffer) {
+  value = (((value + (0x100 - endValue))%0x100) / interval);
+  if (reverse) {
+    value = 100 - value;
+  }
+  if (value > 100) {
+    // overflow
+    if (value <= (100 + buffer)) {
+      value = 100;
+    // underflow (value is unsigned)
+    } else {
+      value = 0;
+    }
+  }
+  return value;
+}

+ 20 - 0
lib/MiLight/V2PacketFormatter.h

@@ -10,6 +10,9 @@
 #define V2_COMMAND_INDEX 4
 #define V2_ARGUMENT_INDEX 5
 
+// Default number of values to allow before and after strictly defined range for V2 scales
+#define V2_DEFAULT_RANGE_BUFFER 0x13
+
 class V2PacketFormatter : public PacketFormatter {
 public:
   V2PacketFormatter(uint8_t protocolId, uint8_t numGroups);
@@ -26,6 +29,23 @@ public:
 
   uint8_t groupCommandArg(MiLightStatus status, uint8_t groupId);
 
+  /*
+   * Some protocols have scales which have the following characteristics:
+   *   Start at some value X, goes down to 0, then up to Y.
+   * eg:
+   *   0x8F, 0x8D, ..., 0, 0x2, ..., 0x20
+   * This is a parameterized method to convert from [0, 100] TO this scale
+   */
+  static uint8_t tov2scale(uint8_t value, uint8_t endValue, uint8_t interval, bool reverse = true);
+
+  /*
+   * Method to convert FROM the scale described above to [0, 100].
+   * 
+   * An extra parameter is exposed: `buffer`, which allows for a range of values before/after the 
+   * max that will be mapped to 0 and 100, respectively.
+   */
+  static uint8_t fromv2scale(uint8_t value, uint8_t endValue, uint8_t interval, bool reverse = true, uint8_t buffer = V2_DEFAULT_RANGE_BUFFER);
+
 protected:
   const uint8_t protocolId;
   const uint8_t numGroups;

+ 2 - 1
lib/Radio/MiLightConstants.h

@@ -7,7 +7,8 @@ enum MiLightRemoteType {
   REMOTE_TYPE_CCT     = 1,
   REMOTE_TYPE_RGB_CCT = 2,
   REMOTE_TYPE_RGB     = 3,
-  REMOTE_TYPE_FUT089  = 4
+  REMOTE_TYPE_FUT089  = 4,
+  REMOTE_TYPE_FUT091  = 5
 };
 
 enum MiLightStatus {

+ 2 - 1
lib/Types/MiLightConstants.h

@@ -7,7 +7,8 @@ enum MiLightRemoteType {
   REMOTE_TYPE_CCT     = 1,
   REMOTE_TYPE_RGB_CCT = 2,
   REMOTE_TYPE_RGB     = 3,
-  REMOTE_TYPE_FUT089  = 4
+  REMOTE_TYPE_FUT089  = 4,
+  REMOTE_TYPE_FUT091  = 5
 };
 
 enum MiLightStatus {

+ 2 - 2
lib/Udp/V5MiLightUdpServer.cpp

@@ -85,7 +85,7 @@ void V5MiLightUdpServer::handleCommand(uint8_t command, uint8_t commandArg) {
     uint8_t onOffGroup = CctPacketFormatter::cctCommandIdToGroup(command);
 
     if (onOffGroup != 255) {
-      client->prepare(&FUT091Config, deviceId, onOffGroup);
+      client->prepare(&FUT007Config, deviceId, onOffGroup);
       // Night mode commands are same as off commands with MSB set
       if ((command & 0x80) == 0x80) {
         client->enableNightMode();
@@ -95,7 +95,7 @@ void V5MiLightUdpServer::handleCommand(uint8_t command, uint8_t commandArg) {
       return;
     }
 
-    client->prepare(&FUT091Config, deviceId, lastGroup);
+    client->prepare(&FUT007Config, deviceId, lastGroup);
 
     switch(command) {
       case UDP_CCT_BRIGHTNESS_DOWN:

+ 1 - 1
lib/Udp/V6CctCommandHandler.h

@@ -18,7 +18,7 @@ enum CctCommandIds {
 class V6CctCommandHandler : public V6CommandHandler {
 public:
   V6CctCommandHandler()
-    : V6CommandHandler(0x0100, FUT091Config)
+    : V6CommandHandler(0x0100, FUT007Config)
   { }
 
   virtual bool handleCommand(

+ 98 - 2
test/d1_mini/test_state.cpp

@@ -2,13 +2,107 @@
 
 #include <FS.h>
 #include <Arduino.h>
-#include "unity.h"
 
 #include <GroupState.h>
 #include <GroupStateStore.h>
 #include <GroupStateCache.h>
 #include <GroupStatePersistence.h>
 
+#include <RgbCctPacketFormatter.h>
+#include <FUT091PacketFormatter.h>
+#include <Units.h>
+
+#include "unity.h"
+
+//================================================================================
+// Packet formatter
+//================================================================================
+
+template <typename T>
+void run_packet_test(uint8_t* packet, PacketFormatter* packetFormatter, const BulbId& expectedBulbId, const String& expectedKey, const T expectedValue) {
+  GroupStateStore stateStore(10, 0);
+  Settings settings;
+  RgbCctPacketFormatter formatter;
+  DynamicJsonBuffer jsonBuffer;
+  JsonObject& result = jsonBuffer.createObject();
+
+  packetFormatter->prepare(0, 0, &stateStore, &settings);
+  BulbId bulbId = packetFormatter->parsePacket(packet, result);
+
+  TEST_ASSERT_EQUAL_INT_MESSAGE(expectedBulbId.deviceId, bulbId.deviceId, "Should get the expected device ID");
+  TEST_ASSERT_EQUAL_INT_MESSAGE(expectedBulbId.groupId, bulbId.groupId, "Should get the expected group ID");
+  TEST_ASSERT_EQUAL_INT_MESSAGE(expectedBulbId.deviceType, bulbId.deviceType, "Should get the expected remote type");
+
+  TEST_ASSERT_TRUE_MESSAGE(result.containsKey(expectedKey), "Parsed packet should be for expected command type");
+  TEST_ASSERT_TRUE_MESSAGE(result[expectedKey] == expectedValue, "Parsed packet should have expected value");
+}
+
+void test_fut092_packet_formatter() {
+  RgbCctPacketFormatter packetFormatter;
+
+  uint8_t onPacket[] = {0x00, 0xDB, 0xE1, 0x24, 0x66, 0xCA, 0x54, 0x66, 0xD2};
+  run_packet_test(
+    onPacket, 
+    &packetFormatter, 
+    BulbId(1, 1, REMOTE_TYPE_RGB_CCT), 
+    "state", 
+    "OFF"
+  );
+
+  uint8_t minColorTempPacket[] = {0x00, 0xDB, 0xE1, 0x24, 0x64, 0x3C, 0x47, 0x66, 0x31};
+  run_packet_test(
+    minColorTempPacket, 
+    &packetFormatter, 
+    BulbId(1, 1, REMOTE_TYPE_RGB_CCT), 
+    "color_temp", 
+    COLOR_TEMP_MIN_MIREDS
+  );
+
+  uint8_t maxColorTempPacket[] = {0x00, 0xDB, 0xE1, 0x24, 0x64, 0x94, 0x62, 0x66, 0x88};
+  run_packet_test(
+    maxColorTempPacket, 
+    &packetFormatter, 
+    BulbId(1, 1, REMOTE_TYPE_RGB_CCT), 
+    "color_temp", 
+    COLOR_TEMP_MAX_MIREDS
+  );
+}
+
+void test_fut091_packet_formatter() {
+  FUT091PacketFormatter packetFormatter;
+
+  uint8_t onPacket[] = {0x00, 0xDC, 0xE1, 0x24, 0x66, 0xCA, 0xBA, 0x66, 0xB5};
+  run_packet_test(
+    onPacket, 
+    &packetFormatter, 
+    BulbId(1, 1, REMOTE_TYPE_FUT091), 
+    "state", 
+    "OFF"
+  );
+
+  uint8_t minColorTempPacket[] = {0x00, 0xDC, 0xE1, 0x24, 0x64, 0x8D, 0xB9, 0x66, 0x71};
+  run_packet_test(
+    minColorTempPacket, 
+    &packetFormatter, 
+    BulbId(1, 1, REMOTE_TYPE_FUT091), 
+    "color_temp", 
+    COLOR_TEMP_MIN_MIREDS
+  );
+
+  uint8_t maxColorTempPacket[] = {0x00, 0xDC, 0xE1, 0x24, 0x64, 0x55, 0xB7, 0x66, 0x27};
+  run_packet_test(
+    maxColorTempPacket, 
+    &packetFormatter, 
+    BulbId(1, 1, REMOTE_TYPE_FUT091), 
+    "color_temp", 
+    COLOR_TEMP_MAX_MIREDS
+  );
+}
+
+//================================================================================
+// Group State
+//================================================================================
+
 GroupState color() {
   GroupState s;
 
@@ -241,6 +335,9 @@ void setup() {
   RUN_TEST(test_store);
   RUN_TEST(test_group_0);
 
+  RUN_TEST(test_fut091_packet_formatter);
+  RUN_TEST(test_fut092_packet_formatter);
+
   UNITY_END();
 }
 
@@ -248,5 +345,4 @@ void loop() {
   // nothing to be done here.
 }
 
-
 // #endif

+ 1 - 0
web/src/css/style.css

@@ -16,6 +16,7 @@ label:not(.error) .error-info { display: none; }
 #content .dropdown-menu li { display: block; }
 #traffic-sniff { display: none; }
 #sniffed-traffic { max-height: 50em; overflow-y: auto; }
+.dropdown label { display: inline-block; }
 .navbar { background-color: rgba(41,41,45,.9) !important; border-radius: 0 }
 .navbar-brand, .navbar-brand:hover { color: white; }
 .btn-secondary {

+ 33 - 4
web/src/index.html

@@ -77,7 +77,7 @@
       </div>
 
       <div class="col-sm-4">
-        <div class="mode-option" id="group-option" data-for="cct,rgbw,rgb_cct,fut089">
+        <div class="mode-option" id="group-option" data-for="cct,rgbw,rgb_cct,fut089,fut091">
           <label for="groupId">Group</label>
 
           <div class="btn-group" id="groupId" data-toggle="buttons">
@@ -113,9 +113,9 @@
       </div>
 
       <div class="col-sm-4">
-        <label for="mode">Mode</label>
+        <label for="mode">Remote Type</label>
 
-        <div class="btn-group" id="mode" data-toggle="buttons">
+        <!-- <div class="btn-group" id="mode" data-toggle="buttons">
           <label class="btn btn-secondary active">
             <input type="radio" name="mode" autocomplete="off" data-value="rgbw" checked> RGBW
           </label>
@@ -131,6 +131,35 @@
           <label class="btn btn-secondary">
             <input type="radio" name="mode" autocomplete="off" data-value="fut089"> FUT089
           </label>
+          <label class="btn btn-secondary">
+            <input type="radio" name="mode" autocomplete="off" data-value="fut091"> FUT091
+          </label>
+        </div> -->
+        <div class="dropdown">
+          <button class="btn btn-default dropdown-toggle" type="button" data-toggle="dropdown">
+            <label>Mode</label>
+            <span class="caret"></span>
+          </button>
+          <ul class="dropdown-menu" id="mode">
+            <li data-value="rgbw">
+              <a href="#">RGBW</a>
+            </li>
+            <li data-value="cct">
+              <a href="#">CCT</a>
+            </li>
+            <li data-value="rgb_cct" class="active">
+              <a href="#">RGB+CCT</a>
+            </li>
+            <li data-value="rgb">
+              <a href="#">RGB</a>
+            </li>
+            <li data-value="fut089">
+              <a href="#">FUT089 / B8</a>
+            </li>
+            <li data-value="fut091">
+              <a href="#">FUT091 / B2</a>
+            </li>
+          </ul>
         </div>
       </div>
     </div>
@@ -171,7 +200,7 @@
       </div>
     </div>
 
-    <div class="mode-option" data-for="cct,rgb_cct,fut089">
+    <div class="mode-option" data-for="cct,rgb_cct,fut089,fut091">
       <div class="row">
         <div class="col-sm-12">
           <h5>Color Temperature</h5>

+ 13 - 3
web/src/js/script.js

@@ -288,7 +288,7 @@ var activeUrl = function() {
 }
 
 var getCurrentMode = function() {
-  return $('input[name="mode"]:checked').data('value');
+  return $('#mode li.active').data('value');
 };
 
 var updateGroup = _.throttle(
@@ -476,7 +476,10 @@ var deviceIdError = function(v) {
 };
 
 var updateModeOptions = function() {
-  var currentMode = getCurrentMode();
+  var currentMode = getCurrentMode()
+    , modeLabel = $('#mode li[data-value="' + currentMode + '"] a').html();
+
+  $('label', $('#mode').closest('.dropdown')).html(modeLabel);
 
   $('.mode-option').map(function() {
     if ($(this).data('for').split(',').includes(currentMode)) {
@@ -720,7 +723,14 @@ $(function() {
     $('#gateway-server-configs').append(gatewayServerRow('', ''));
   });
 
-  $('#mode').change(updateModeOptions);
+  $('#mode li').click(function(e) {
+    e.preventDefault();
+
+    $('li', $(this).parent()).removeClass('active');
+    $(this).addClass('active');
+
+    updateModeOptions.bind(this)();
+  });
 
   $('body').on('click', '.remove-gateway-server', function() {
     $(this).closest('tr').remove();