Parcourir la source

Merge pull request #7 from sidoh/cct

Add support for CCT (Adjustable white) bulbs
Chris Mullins il y a 8 ans
Parent
commit
c4aec0715a

+ 52 - 0
lib/MiLight/MiLightButtons.h

@@ -0,0 +1,52 @@
+#ifndef _MILIGHT_BUTTONS
+#define _MILIGHT_BUTTONS 
+
+enum MiLightRgbwButton {
+  RGBW_ALL_ON            = 0x01,
+  RGBW_ALL_OFF           = 0x02,
+  RGBW_GROUP_1_ON        = 0x03,
+  RGBW_GROUP_1_OFF       = 0x04,
+  RGBW_GROUP_2_ON        = 0x05,
+  RGBW_GROUP_2_OFF       = 0x06,
+  RGBW_GROUP_3_ON        = 0x07,
+  RGBW_GROUP_3_OFF       = 0x08,
+  RGBW_GROUP_4_ON        = 0x09,
+  RGBW_GROUP_4_OFF       = 0x0A,
+  RGBW_SPEED_UP          = 0x0B, 
+  RGBW_SPEED_DOWN        = 0x0C, 
+  RGBW_DISCO_MODE        = 0x0D,
+  RGBW_BRIGHTNESS        = 0x0E,
+  RGBW_COLOR             = 0x0F,
+  RGBW_ALL_MAX_LEVEL     = 0x11,
+  RGBW_ALL_MIN_LEVEL     = 0x12,
+  
+  // These are the only mechanism (that I know of) to disable RGB and set the
+  // color to white.
+  RGBW_GROUP_1_MAX_LEVEL = 0x13,
+  RGBW_GROUP_1_MIN_LEVEL = 0x14,
+  RGBW_GROUP_2_MAX_LEVEL = 0x15,
+  RGBW_GROUP_2_MIN_LEVEL = 0x16,
+  RGBW_GROUP_3_MAX_LEVEL = 0x17,
+  RGBW_GROUP_3_MIN_LEVEL = 0x18,
+  RGBW_GROUP_4_MAX_LEVEL = 0x19,
+  RGBW_GROUP_4_MIN_LEVEL = 0x1A,
+};
+
+enum MiLightCctButton {
+  CCT_ALL_ON            = 0x05,
+  CCT_ALL_OFF           = 0x09,
+  CCT_GROUP_1_ON        = 0x08,
+  CCT_GROUP_1_OFF       = 0x0B,
+  CCT_GROUP_2_ON        = 0x0D,
+  CCT_GROUP_2_OFF       = 0x03,
+  CCT_GROUP_3_ON        = 0x07,
+  CCT_GROUP_3_OFF       = 0x0A,
+  CCT_GROUP_4_ON        = 0x02,
+  CCT_GROUP_4_OFF       = 0x06,
+  CCT_BRIGHTNESS_DOWN   = 0x04,
+  CCT_BRIGHTNESS_UP     = 0x0C,
+  CCT_TEMPERATURE_UP    = 0x0E,
+  CCT_TEMPERATURE_DOWN  = 0x0F
+};
+
+#endif

+ 208 - 73
lib/MiLight/MiLightClient.cpp

@@ -1,89 +1,110 @@
 #include <MiLightClient.h>
+#include <MiLightRadioConfig.h>
 
-void MiLightClient::deserializePacket(const uint8_t rawPacket[], MiLightPacket& packet) {
-  uint8_t ptr = 0;
+MiLightRadio* MiLightClient::getRadio(const MiLightRadioType type) {
+  MiLightRadio* radio = NULL;
   
-  packet.deviceType = rawPacket[ptr++];
-  packet.deviceId = (rawPacket[ptr++] << 8) | rawPacket[ptr++];
-  packet.color = rawPacket[ptr++];
-  
-  packet.brightness = rawPacket[ptr] >> 3;
-  packet.groupId = rawPacket[ptr++] & 0x07;
-  
-  packet.button = rawPacket[ptr++];
-  packet.sequenceNum = rawPacket[ptr++];
-}
-
-void MiLightClient::serializePacket(uint8_t rawPacket[], const MiLightPacket& packet) {
-  uint8_t ptr = 0;
-  
-  rawPacket[ptr++] = packet.deviceType;
+  if (type == RGBW) {
+    return rgbwRadio->getRadio();
+  } else if (type == CCT) {
+    return cctRadio->getRadio();
+  } else if (type == RGBW_CCT) {
+    return rgbwCctRadio->getRadio();
+  }
   
-  // big endian
-  rawPacket[ptr++] = packet.deviceId >> 8;
-  rawPacket[ptr++] = packet.deviceId & 0xFF;
+  if (radio != NULL) {
+    radio->configure();
+  }
   
-  rawPacket[ptr++] = packet.color;
-  rawPacket[ptr++] = (packet.brightness << 3) | (packet.groupId & 0x07);
-  rawPacket[ptr++] = packet.button;
-  rawPacket[ptr++] = packet.sequenceNum;
+  return radio;
 }
 
 uint8_t MiLightClient::nextSequenceNum() {
   return sequenceNum++;
 }
 
-bool MiLightClient::available() {
+bool MiLightClient::available(const MiLightRadioType radioType) {
+  MiLightRadio* radio = getRadio(radioType);
+  radio->begin();
+  
+  if (radio == NULL) {
+    return false;
+  }
+  
   return radio->available();
 }
 
-void MiLightClient::read(MiLightPacket& packet) {
-  uint8_t packetBytes[MILIGHT_PACKET_LENGTH];
+void MiLightClient::read(const MiLightRadioType radioType, uint8_t packet[]) {
+  MiLightRadio* radio = getRadio(radioType);
+  
+  if (radio == NULL) {
+    return;
+  }
+  
   size_t length;
-  radio->read(packetBytes, length);
-  deserializePacket(packetBytes, packet);
+  radio->read(packet, length);
 }
 
-void MiLightClient::write(MiLightPacket& packet, const unsigned int resendCount) {
-  uint8_t packetBytes[MILIGHT_PACKET_LENGTH];
-  serializePacket(packetBytes, packet);
+void MiLightClient::write(const MiLightRadioType radioType, 
+  uint8_t packet[],
+  const unsigned int resendCount) {
+    
+  MiLightRadio* radio = getRadio(radioType);
+  
+  if (radio == NULL) {
+    return;
+  }
   
   for (int i = 0; i < resendCount; i++) {
-    radio->write(packetBytes, MILIGHT_PACKET_LENGTH);
+    radio->write(packet, MILIGHT_PACKET_LENGTH);
+    yield();
   }
 }
 
-
-void MiLightClient::write(
+void MiLightClient::writeRgbw(
   const uint16_t deviceId,
   const uint8_t color,
   const uint8_t brightness,
   const uint8_t groupId,
-  const MiLightButton button) {
-    
-  // Expect an input value in [0, 100]. Map it down to [0, 25].
-  const uint8_t adjustedBrightness = round(brightness * (25 / 100.0));
+  const uint8_t button,
+  const unsigned int resendCount) {
   
-  // The actual protocol uses a bizarre range where min is 16, max is 23:
-  // [16, 15, ..., 0, 31, ..., 23]
-  const uint8_t packetBrightnessValue = (
-    ((31 - adjustedBrightness) + 17) % 32
-  );
+  uint8_t packet[MilightRgbwConfig.packetLength];
+  size_t packetPtr = 0;
   
-  MiLightPacket packet;
-  packet.deviceType = MiLightDeviceType::RGBW;
-  packet.deviceId = deviceId;
-  packet.color = color;
-  packet.brightness = packetBrightnessValue;
-  packet.groupId = groupId;
-  packet.button = button;
-  packet.sequenceNum = nextSequenceNum();
+  packet[packetPtr++] = RGBW;
+  packet[packetPtr++] = deviceId >> 8;
+  packet[packetPtr++] = deviceId & 0xFF;
+  packet[packetPtr++] = color;
+  packet[packetPtr++] = (brightness << 3) | (groupId & 0x07);
+  packet[packetPtr++] = button;
+  packet[packetPtr++] = nextSequenceNum();
   
-  write(packet);
+  write(RGBW, packet, resendCount);
+}
+
+void MiLightClient::writeCct(const uint16_t deviceId,
+  const uint8_t groupId,
+  const uint8_t button,
+  const unsigned int resendCount) {
+    
+  uint8_t packet[MilightRgbwConfig.packetLength];
+  uint8_t sequenceNum = nextSequenceNum();
+  size_t packetPtr = 0;
+  
+  packet[packetPtr++] = CCT;
+  packet[packetPtr++] = deviceId >> 8;
+  packet[packetPtr++] = deviceId & 0xFF;
+  packet[packetPtr++] = groupId;
+  packet[packetPtr++] = button;
+  packet[packetPtr++] = sequenceNum;
+  packet[packetPtr++] = sequenceNum;
+  
+  write(CCT, packet, resendCount);
 }
     
 void MiLightClient::updateColorRaw(const uint16_t deviceId, const uint8_t groupId, const uint16_t color) {
-  write(deviceId, color, 0, groupId, COLOR);
+  writeRgbw(deviceId, color, 0, groupId, RGBW_COLOR);
 }
 
 void MiLightClient::updateHue(const uint16_t deviceId, const uint8_t groupId, const uint16_t hue) {
@@ -92,41 +113,155 @@ void MiLightClient::updateHue(const uint16_t deviceId, const uint8_t groupId, co
   const int16_t remappedColor = (hue + 40) % 360;
   const uint8_t adjustedColor = round(remappedColor * (255 / 360.0));
   
-  write(deviceId, adjustedColor, 0, groupId, COLOR);
+  writeRgbw(deviceId, adjustedColor, 0, groupId, RGBW_COLOR);
 }
 
 void MiLightClient::updateBrightness(const uint16_t deviceId, const uint8_t groupId, const uint8_t brightness) {
-  write(deviceId, 0, brightness, groupId, BRIGHTNESS);
+  // Expect an input value in [0, 100]. Map it down to [0, 25].
+  const uint8_t adjustedBrightness = round(brightness * (25 / 100.0));
+  
+  // The actual protocol uses a bizarre range where min is 16, max is 23:
+  // [16, 15, ..., 0, 31, ..., 23]
+  const uint8_t packetBrightnessValue = (
+    ((31 - adjustedBrightness) + 17) % 32
+  );
+  
+  writeRgbw(deviceId, 0, packetBrightnessValue, groupId, RGBW_BRIGHTNESS);
 }
 
-void MiLightClient::updateStatus(const uint16_t deviceId, const uint8_t groupId, MiLightStatus status) {
-  uint8_t button = MiLightButton::GROUP_1_ON + ((groupId - 1)*2) + status;
-  write(deviceId, 0, 0, groupId, static_cast<MiLightButton>(button));
+void MiLightClient::updateStatus(const MiLightRadioType type, const uint16_t deviceId, const uint8_t groupId, MiLightStatus status) {
+  if (type == RGBW) {
+    uint8_t button = RGBW_GROUP_1_ON + ((groupId - 1)*2) + status;
+    writeRgbw(deviceId, 0, 0, groupId, button);
+  } else {
+    writeCct(deviceId, groupId, getCctStatusButton(groupId, status));
+  }
 }
 
 void MiLightClient::updateColorWhite(const uint16_t deviceId, const uint8_t groupId) {
-  uint8_t button = MiLightButton::GROUP_1_MAX_LEVEL + ((groupId - 1)*2);
-  pressButton(deviceId, groupId, static_cast<MiLightButton>(button));
+  uint8_t button = RGBW_GROUP_1_MAX_LEVEL + ((groupId - 1)*2);
+  pressButton(RGBW, deviceId, groupId, button);
 }
 
-void MiLightClient::pair(const uint16_t deviceId, const uint8_t groupId) {
-  updateStatus(deviceId, groupId, ON);
+void MiLightClient::pair(const MiLightRadioType type, const uint16_t deviceId, const uint8_t groupId) {
+  updateStatus(type, deviceId, groupId, ON);
 }
 
-void MiLightClient::unpair(const uint16_t deviceId, const uint8_t groupId) {
-  updateStatus(deviceId, groupId, ON);
-  delay(1);
-  updateColorWhite(deviceId, groupId);
+void MiLightClient::unpair(const MiLightRadioType type, const uint16_t deviceId, const uint8_t groupId) {
+  if (type == RGBW) {
+    updateStatus(RGBW, deviceId, groupId, ON);
+    delay(1);
+    updateColorWhite(deviceId, groupId);
+  } else if (type == CCT) {
+    for (int i = 0; i < 5; i++) {
+      updateStatus(CCT, deviceId, groupId, ON);
+      delay(1);
+    }
+  }
 }
     
-void MiLightClient::pressButton(const uint16_t deviceId, const uint8_t groupId, MiLightButton button) {
-  write(deviceId, 0, 0, groupId, button);
+void MiLightClient::pressButton(const MiLightRadioType type, const uint16_t deviceId, const uint8_t groupId, const uint8_t button) {
+  if (type == RGBW) {
+    writeRgbw(deviceId, 0, 0, groupId, button);
+  } else if (type == CCT) {
+    writeCct(deviceId, groupId, button);
+  }
+}
+
+void MiLightClient::allOn(const MiLightRadioType type, const uint16_t deviceId) {
+  if (type == RGBW) {
+    writeRgbw(deviceId, 0, 0, 0, RGBW_ALL_ON);
+  } else if (type == CCT) {
+    writeCct(deviceId, 0, CCT_ALL_ON);
+  }
+}
+
+void MiLightClient::allOff(const MiLightRadioType type, const uint16_t deviceId) {
+  if (type == RGBW) {
+    writeRgbw(deviceId, 0, 0, 0, RGBW_ALL_OFF);
+  } else if (type == CCT) {
+    writeCct(deviceId, 0, CCT_ALL_OFF);
+  }
+}
+
+void MiLightClient::increaseCctBrightness(const uint16_t deviceId, const uint8_t groupId) {
+  writeCct(deviceId, groupId, CCT_BRIGHTNESS_UP, 10);
+}
+
+void MiLightClient::decreaseCctBrightness(const uint16_t deviceId, const uint8_t groupId) {
+  writeCct(deviceId, groupId, CCT_BRIGHTNESS_DOWN, 10);
+}
+
+void MiLightClient::updateCctBrightness(const uint16_t deviceId, const uint8_t groupId, const uint8_t brightness) {
+  for (int i = 0; i < MILIGHT_CCT_INTERVALS; i++) {
+    decreaseCctBrightness(deviceId, groupId);
+  }
+  for (int i = 0; i < brightness/10; i++) {
+    increaseCctBrightness(deviceId, groupId);
+  }
 }
 
-void MiLightClient::allOn(const uint16_t deviceId) {
-  write(deviceId, 0, 0, 0, ALL_ON);
+void MiLightClient::increaseTemperature(const uint16_t deviceId, const uint8_t groupId) {
+  writeCct(deviceId, groupId, CCT_TEMPERATURE_UP, 10);
 }
 
-void MiLightClient::allOff(const uint16_t deviceId) {
-  write(deviceId, 0, 0, 0, ALL_OFF);
+void MiLightClient::decreaseTemperature(const uint16_t deviceId, const uint8_t groupId) {
+  writeCct(deviceId, groupId, CCT_TEMPERATURE_DOWN, 10);
+}
+
+void MiLightClient::updateTemperature(const uint16_t deviceId, const uint8_t groupId, const uint8_t temperature) {
+  for (int i = 0; i < MILIGHT_CCT_INTERVALS; i++) {
+    decreaseTemperature(deviceId, groupId);
+  }
+  for (int i = 0; i < temperature; i++) {
+    increaseTemperature(deviceId, groupId);
+  }
+}
+
+uint8_t MiLightClient::getCctStatusButton(uint8_t groupId, MiLightStatus status) {
+  uint8_t button = 0;
+  
+  if (status == ON) {
+    switch(groupId) {
+      case 1:
+        button = CCT_GROUP_1_ON;
+        break;
+      case 2:
+        button = CCT_GROUP_2_ON;
+        break;
+      case 3:
+        button = CCT_GROUP_3_ON;
+        break;
+      case 4:
+        button = CCT_GROUP_4_ON;
+        break;
+    }
+  } else {
+    switch(groupId) {
+      case 1:
+        button = CCT_GROUP_1_OFF;
+        break;
+      case 2:
+        button = CCT_GROUP_2_OFF;
+        break;
+      case 3:
+        button = CCT_GROUP_3_OFF;
+        break;
+      case 4:
+        button = CCT_GROUP_4_OFF;
+        break;
+    }
+  }
+  
+  return button;
+}
+
+MiLightRadioType MiLightClient::getRadioType(const String& typeName) {
+  if (typeName.equalsIgnoreCase("rgbw")) {
+    return RGBW;
+  } else if (typeName.equalsIgnoreCase("cct")) {
+    return CCT;
+  } else {
+    return UNKNOWN;
+  }
 }

+ 78 - 70
lib/MiLight/MiLightClient.h

@@ -2,114 +2,122 @@
 #include <MiLightRadio.h>
 #include <PL1167_nRF24.h>
 #include <RF24.h>
+#include <MiLightButtons.h>
 
 #ifndef _MILIGHTCLIENT_H
 #define _MILIGHTCLIENT_H
 
 #define MILIGHT_PACKET_LENGTH 7
+#define MILIGHT_CCT_INTERVALS 10
+#define MILIGHT_DEFAULT_RESEND_COUNT 50
 
-enum MiLightDeviceType {
-  WHITE = 0xB0,
-  RGBW = 0xB8
-};
-
-enum MiLightButton {
-  ALL_ON            = 0x01,
-  ALL_OFF           = 0x02,
-  GROUP_1_ON        = 0x03,
-  GROUP_1_OFF       = 0x04,
-  GROUP_2_ON        = 0x05,
-  GROUP_2_OFF       = 0x06,
-  GROUP_3_ON        = 0x07,
-  GROUP_3_OFF       = 0x08,
-  GROUP_4_ON        = 0x09,
-  GROUP_4_OFF       = 0x0A,
-  SPEED_UP          = 0x0B, 
-  SPEED_DOWN        = 0x0C, 
-  DISCO_MODE        = 0x0D,
-  BRIGHTNESS        = 0x0E,
-  COLOR             = 0x0F,
-  ALL_MAX_LEVEL     = 0x11,
-  ALL_MIN_LEVEL     = 0x12,
-  
-  // These are the only mechanism (that I know of) to disable RGB and set the
-  // color to white.
-  GROUP_1_MAX_LEVEL = 0x13,
-  GROUP_1_MIN_LEVEL = 0x14,
-  GROUP_2_MAX_LEVEL = 0x15,
-  GROUP_2_MIN_LEVEL = 0x16,
-  GROUP_3_MAX_LEVEL = 0x17,
-  GROUP_3_MIN_LEVEL = 0x18,
-  GROUP_4_MAX_LEVEL = 0x19,
-  GROUP_4_MIN_LEVEL = 0x1A,
+enum MiLightRadioType {
+  UNKNOWN = 0,
+  RGBW  = 0xB8,
+  CCT   = 0x5A,
+  RGBW_CCT = 0x99
 };
 
 enum MiLightStatus { ON = 0, OFF = 1 };
+
+class MiLightRadioStack {
+public:
+  MiLightRadioStack(RF24& rf, const MiLightRadioConfig& config) {
+    nrf = new PL1167_nRF24(rf);
+    radio = new MiLightRadio(*nrf, config);
+  }
   
-struct MiLightPacket {
-  uint8_t deviceType;
-  uint16_t deviceId;
-  uint8_t color;
-  uint8_t brightness;
-  uint8_t groupId;
-  uint8_t button;
-  uint8_t sequenceNum;
+  ~MiLightRadioStack() {
+    delete radio;
+    delete nrf;
+  }
+  
+  inline MiLightRadio* getRadio() {
+    return this->radio;
+  }
+  
+private:
+  PL1167_nRF24 *nrf;
+  MiLightRadio *radio;
 };
 
 class MiLightClient {
   public:
-    MiLightClient(uint8_t cePin, uint8_t csnPin) :
-      sequenceNum(0) {
-      rf = new RF24(cePin, csnPin);
-      prf = new PL1167_nRF24(*rf);
-      radio = new MiLightRadio(*prf);
+    MiLightClient(uint8_t cePin, uint8_t csnPin)
+    : sequenceNum(0),
+      rf(RF24(cePin, csnPin))
+    {
+      rgbwRadio = new MiLightRadioStack(rf, MilightRgbwConfig);
+      cctRadio = new MiLightRadioStack(rf, MilightCctConfig);
+      rgbwCctRadio = new MiLightRadioStack(rf, MilightRgbwCctConfig);
     }
     
     ~MiLightClient() {
-      delete rf;
-      delete prf;
-      delete radio;
+      delete rgbwRadio;
+      delete cctRadio;
+      delete rgbwCctRadio;
     }
     
     void begin() {
-      radio->begin();
+      rgbwRadio->getRadio()->begin();
+      cctRadio->getRadio()->begin();
+      rgbwCctRadio->getRadio()->begin();
     }
     
-    bool available();
-    void read(MiLightPacket& packet);
-    void write(MiLightPacket& packet, const unsigned int resendCount = 50);
+    bool available(const MiLightRadioType radioType);
+    void read(const MiLightRadioType radioType, uint8_t packet[]);
+    void write(const MiLightRadioType radioType, uint8_t packet[], const unsigned int resendCount = MILIGHT_DEFAULT_RESEND_COUNT);
     
-    void write(
+    void writeRgbw(
       const uint16_t deviceId,
       const uint8_t color,
       const uint8_t brightness,
       const uint8_t groupId,
-      const MiLightButton button
+      const uint8_t button,
+      const unsigned int resendCount = MILIGHT_DEFAULT_RESEND_COUNT
     );
     
-    void updateColorRaw(const uint16_t deviceId, const uint8_t groupId, const uint16_t color);
+    void writeCct(
+      const uint16_t deviceId,
+      const uint8_t groupId,
+      const uint8_t button,
+      const unsigned int resendCount = MILIGHT_DEFAULT_RESEND_COUNT
+    );
     
+    // Common methods
+    void updateStatus(const MiLightRadioType type,const uint16_t deviceId, const uint8_t groupId, MiLightStatus status);
+    void pair(const MiLightRadioType type,const uint16_t deviceId, const uint8_t groupId);
+    void unpair(const MiLightRadioType type,const uint16_t deviceId, const uint8_t groupId);
+    void allOn(const MiLightRadioType type,const uint16_t deviceId);
+    void allOff(const MiLightRadioType type,const uint16_t deviceId);
+    void pressButton(const MiLightRadioType type, const uint16_t deviceId, const uint8_t groupId, uint8_t button);
+    
+    // RGBW methods
     void updateHue(const uint16_t deviceId, const uint8_t groupId, const uint16_t hue);
     void updateBrightness(const uint16_t deviceId, const uint8_t groupId, const uint8_t brightness);
-    void updateStatus(const uint16_t deviceId, const uint8_t groupId, MiLightStatus status);
     void updateColorWhite(const uint16_t deviceId, const uint8_t groupId);
-    void pair(const uint16_t deviceId, const uint8_t groupId);
-    void unpair(const uint16_t deviceId, const uint8_t groupId);
-    
-    void allOn(const uint16_t deviceId);
-    void allOff(const uint16_t deviceId);
+    void updateColorRaw(const uint16_t deviceId, const uint8_t groupId, const uint16_t color);
+
+    // CCT methods
+    void updateTemperature(const uint16_t deviceId, const uint8_t groupId, const uint8_t colorTemperature);
+    void decreaseTemperature(const uint16_t deviceId, const uint8_t groupId);
+    void increaseTemperature(const uint16_t deviceId, const uint8_t groupId);
+    void updateCctBrightness(const uint16_t deviceId, const uint8_t groupId, const uint8_t brightness);
+    void decreaseCctBrightness(const uint16_t deviceId, const uint8_t groupId);
+    void increaseCctBrightness(const uint16_t deviceId, const uint8_t groupId);
     
-    void pressButton(const uint16_t deviceId, const uint8_t groupId, MiLightButton button);
+    MiLightRadio* getRadio(const MiLightRadioType type);
     
-    static void deserializePacket(const uint8_t rawPacket[], MiLightPacket& packet);
-    static void serializePacket(uint8_t rawPacket[], const MiLightPacket& packet);
+    static uint8_t getCctStatusButton(uint8_t groupId, MiLightStatus status);
+    static MiLightRadioType getRadioType(const String& typeName);
     
   private:
-    RF24 *rf;
-    PL1167_nRF24 *prf;
-    MiLightRadio* radio;
-    uint8_t sequenceNum;
+    RF24 rf;
+    MiLightRadioStack* rgbwRadio;
+    MiLightRadioStack* cctRadio;
+    MiLightRadioStack* rgbwCctRadio;
     
+    uint8_t sequenceNum;
     uint8_t nextSequenceNum();
 };
 

+ 27 - 15
lib/MiLight/MiLightRadio.cpp

@@ -9,11 +9,8 @@
 
 #define PACKET_ID(packet) ( ((packet[1] & 0xF0)<<24) | (packet[2]<<16) | (packet[3]<<8) | (packet[7]) )
 
-static const uint8_t CHANNELS[] = {9, 40, 71};
-#define NUM_CHANNELS (sizeof(CHANNELS)/sizeof(CHANNELS[0]))
-
-MiLightRadio::MiLightRadio(AbstractPL1167 &pl1167)
-  : _pl1167(pl1167) {
+MiLightRadio::MiLightRadio(AbstractPL1167 &pl1167, const MiLightRadioConfig& config)
+  : _pl1167(pl1167), config(config) {
   _waiting = false;
 }
 
@@ -23,8 +20,19 @@ int MiLightRadio::begin()
   if (retval < 0) {
     return retval;
   }
+  
+  retval = configure();
+  if (retval < 0) {
+    return retval;
+  }
+
+  available();
+
+  return 0;
+}
 
-  retval = _pl1167.setCRC(true);
+int MiLightRadio::configure() {
+  int retval = _pl1167.setCRC(true);
   if (retval < 0) {
     return retval;
   }
@@ -39,7 +47,7 @@ int MiLightRadio::begin()
     return retval;
   }
 
-  retval = _pl1167.setSyncword(0x147A, 0x258B);
+  retval = _pl1167.setSyncword(config.syncword0, config.syncword3);
   if (retval < 0) {
     return retval;
   }
@@ -48,19 +56,22 @@ int MiLightRadio::begin()
   if (retval < 0) {
     return retval;
   }
-
-  available();
-
+  
   return 0;
 }
 
 bool MiLightRadio::available()
 {
+  configure();
+  
   if (_waiting) {
+#ifdef DEBUG_PRINTF
+  printf("_waiting\n");
+#endif
     return true;
   }
-
-  if (_pl1167.receive(CHANNELS[0]) > 0) {
+  
+  if (_pl1167.receive(config.channels[0]) > 0) {
     size_t packet_length = sizeof(_packet);
     if (_pl1167.readFIFO(_packet, packet_length) < 0) {
       return false;
@@ -68,7 +79,6 @@ bool MiLightRadio::available()
     if (packet_length == 0 || packet_length != _packet[0] + 1U) {
       return false;
     }
-
     uint32_t packet_id = PACKET_ID(_packet);
     if (packet_id == _prev_packet_id) {
       _dupes_received++;
@@ -113,6 +123,8 @@ int MiLightRadio::write(uint8_t frame[], size_t frame_length)
   if (frame_length > sizeof(_out_packet) - 1) {
     return -1;
   }
+  
+  configure();
 
   memcpy(_out_packet + 1, frame, frame_length);
   _out_packet[0] = frame_length;
@@ -126,9 +138,9 @@ int MiLightRadio::write(uint8_t frame[], size_t frame_length)
 
 int MiLightRadio::resend()
 {
-  for (size_t i = 0; i < NUM_CHANNELS; i++) {
+  for (size_t i = 0; i < config.numChannels; i++) {
     _pl1167.writeFIFO(_out_packet, _out_packet[0] + 1);
-    _pl1167.transmit(CHANNELS[i]);
+    _pl1167.transmit(config.channels[i]);
   }
   return 0;
 }

+ 5 - 1
lib/MiLight/MiLightRadio.h

@@ -14,21 +14,25 @@
 #endif
 
 #include "AbstractPL1167.h"
+#include <MiLightRadioConfig.h>
 
 #ifndef MILIGHTRADIO_H_
 #define MILIGHTRADIO_H_
 
 class MiLightRadio {
   public:
-    MiLightRadio(AbstractPL1167 &pl1167);
+    MiLightRadio(AbstractPL1167 &pl1167, const MiLightRadioConfig& config);
+    
     int begin();
     bool available();
     int read(uint8_t frame[], size_t &frame_length);
     int dupesReceived();
     int write(uint8_t frame[], size_t frame_length);
     int resend();
+    int configure();
   private:
     AbstractPL1167 &_pl1167;
+    const MiLightRadioConfig& config;
     uint32_t _prev_packet_id;
 
     uint8_t _packet[8], _out_packet[8];

+ 42 - 0
lib/MiLight/MiLightRadioConfig.h

@@ -0,0 +1,42 @@
+#include <Arduino.h>
+
+#ifndef _MILIGHT_RADIO_CONFIG
+#define _MILIGHT_RADIO_CONFIG 
+
+class MiLightRadioConfig {
+public:
+  MiLightRadioConfig(const uint16_t syncword0,
+  const uint16_t syncword3,
+  const size_t packetLength,
+  const uint8_t* channels,
+  const size_t numChannels) 
+    : syncword0(syncword0),
+      syncword3(syncword3),
+      packetLength(packetLength),
+      channels(channels),
+      numChannels(numChannels)
+  {}
+    
+  const uint16_t syncword0;
+  const uint16_t syncword3;
+  const size_t packetLength;
+  const uint8_t* channels;
+  const size_t numChannels;
+};
+
+const uint8_t RGBW_CHANNELS[] = {9, 40, 71};
+static MiLightRadioConfig MilightRgbwConfig(
+  0x147A, 0x258B, 7, RGBW_CHANNELS, 3
+);
+
+const uint8_t CCT_CHANNELS[] = {4, 39, 74};
+static MiLightRadioConfig MilightCctConfig(
+  0x050A, 0x55AA, 7, CCT_CHANNELS, 3
+);
+
+const uint8_t RGBWCCT_CHANNELS[] = {70, 39, 8};
+static MiLightRadioConfig MilightRgbwCctConfig(
+  0x7236, 0x1809, 8, RGBWCCT_CHANNELS, 3
+);
+
+#endif

+ 80 - 21
lib/MiLight/MiLightUdpServer.cpp

@@ -34,45 +34,54 @@ void MiLightUdpServer::handleClient() {
 }
 
 void MiLightUdpServer::handleCommand(uint8_t command, uint8_t commandArg) {
-  if (command >= UDP_GROUP_1_ON && command <= UDP_GROUP_4_OFF) {
+  if (command >= UDP_RGBW_GROUP_1_ON && command <= UDP_RGBW_GROUP_4_OFF) {
     const MiLightStatus status = (command % 2) == 1 ? ON : OFF;
-    const uint8_t groupId = (command - UDP_GROUP_1_ON + 2)/2;
+    const uint8_t groupId = (command - UDP_RGBW_GROUP_1_ON + 2)/2;
     
-    client->updateStatus(deviceId, groupId, status);
+    client->updateStatus(RGBW, deviceId, groupId, status);
     
     this->lastGroup = groupId;
-  } else if (command >= UDP_GROUP_ALL_WHITE && command <= UDP_GROUP_4_WHITE) {
-    const uint8_t groupId = (command - UDP_GROUP_ALL_WHITE)/2;
+  } else if (command >= UDP_RGBW_GROUP_ALL_WHITE && command <= UDP_RGBW_GROUP_4_WHITE) {
+    const uint8_t groupId = (command - UDP_RGBW_GROUP_ALL_WHITE)/2;
     client->updateColorWhite(deviceId, groupId);
     this->lastGroup = groupId;
-  } else {
+  } else if (uint8_t cctGroup = cctCommandIdToGroup(command)) {
+    client->updateStatus(
+      CCT,
+      deviceId,
+      cctGroup,
+      cctCommandToStatus(command)
+    );
+    this->lastGroup = cctGroup;
+  }
+  else {
     switch (command) {
-      case UDP_ALL_ON:
-        client->allOn(deviceId);
+      case UDP_RGBW_ALL_ON:
+        client->allOn(RGBW, deviceId);
         break;
       
-      case UDP_ALL_OFF:
-        client->allOff(deviceId);
+      case UDP_RGBW_ALL_OFF:
+        client->allOff(RGBW, deviceId);
         break;
       
-      case UDP_COLOR:
+      case UDP_RGBW_COLOR:
         // UDP color is shifted by 0xC8 from 2.4 GHz color ...
         client->updateColorRaw(deviceId, this->lastGroup, commandArg + 0xC8);
         break;
         
-      case UDP_DISCO_MODE:
-        pressButton(this->lastGroup, DISCO_MODE);
+      case UDP_RGBW_DISCO_MODE:
+        pressButton(this->lastGroup, RGBW_DISCO_MODE);
         break;
         
-      case UDP_SPEED_DOWN:
-        pressButton(this->lastGroup, SPEED_DOWN);
+      case UDP_RGBW_SPEED_DOWN:
+        pressButton(this->lastGroup, RGBW_SPEED_DOWN);
         break;
         
-      case UDP_SPEED_UP:
-        pressButton(this->lastGroup, SPEED_UP);
+      case UDP_RGBW_SPEED_UP:
+        pressButton(this->lastGroup, RGBW_SPEED_UP);
         break;
         
-      case UDP_BRIGHTNESS:
+      case UDP_RGBW_BRIGHTNESS:
         // map [2, 27] --> [0, 100]
         client->updateBrightness(
           deviceId, 
@@ -81,6 +90,22 @@ void MiLightUdpServer::handleCommand(uint8_t command, uint8_t commandArg) {
         );
         break;
         
+      case UDP_CCT_BRIGHTNESS_DOWN:
+        client->decreaseCctBrightness(deviceId, this->lastGroup);
+        break;
+        
+      case UDP_CCT_BRIGHTNESS_UP:
+        client->increaseCctBrightness(deviceId, this->lastGroup);
+        break;
+        
+      case UDP_CCT_TEMPERATURE_DOWN:
+        client->decreaseTemperature(deviceId, this->lastGroup);
+        break;
+        
+      case UDP_CCT_TEMPERATURE_UP:
+        client->increaseTemperature(deviceId, this->lastGroup);
+        break;
+        
       default:
         Serial.print("MiLightUdpServer - Unhandled command: ");
         Serial.println(command);
@@ -88,6 +113,40 @@ void MiLightUdpServer::handleCommand(uint8_t command, uint8_t commandArg) {
   }
 }
 
-void MiLightUdpServer::pressButton(uint8_t group, MiLightButton button) {
-  client->write(deviceId, 0, 0, group, button);
-}  
+void MiLightUdpServer::pressButton(uint8_t group, uint8_t button) {
+  client->writeRgbw(deviceId, 0, 0, group, button);
+}  
+
+uint8_t MiLightUdpServer::cctCommandIdToGroup(uint8_t command) {
+  switch (command) {
+    case UDP_CCT_GROUP_1_ON:
+    case UDP_CCT_GROUP_1_OFF:
+      return 1;
+    case UDP_CCT_GROUP_2_ON:
+    case UDP_CCT_GROUP_2_OFF:
+      return 2;
+    case UDP_CCT_GROUP_3_ON:
+    case UDP_CCT_GROUP_3_OFF:
+      return 3;
+    case UDP_CCT_GROUP_4_ON:
+    case UDP_CCT_GROUP_4_OFF:
+      return 4;
+  }
+  
+  return 0;
+}  
+  
+MiLightStatus MiLightUdpServer::cctCommandToStatus(uint8_t command) {
+  switch (command) {
+    case UDP_CCT_GROUP_1_ON:
+    case UDP_CCT_GROUP_2_ON:
+    case UDP_CCT_GROUP_3_ON:
+    case UDP_CCT_GROUP_4_ON:
+      return ON;
+    case UDP_CCT_GROUP_1_OFF:
+    case UDP_CCT_GROUP_2_OFF:
+    case UDP_CCT_GROUP_3_OFF:
+    case UDP_CCT_GROUP_4_OFF:
+      return OFF;
+  }
+}

+ 38 - 26
lib/MiLight/MiLightUdpServer.h

@@ -2,38 +2,48 @@
 #include <MiLightClient.h>
 #include <WiFiUdp.h>
 
+// This protocol is documented here:
+// http://www.limitlessled.com/dev/
+
 #define MILIGHT_PACKET_BUFFER_SIZE 10
 
 #ifndef _MILIGHT_UDP_SERVER
 #define _MILIGHT_UDP_SERVER 
 
-// These are mostly a remapping of MiLightButton
 enum MiLightUdpCommands {
-  UDP_ALL_ON            = 0x41,
-  UDP_ALL_OFF           = 0x42,
-  
-  UDP_SPEED_UP          = 0x43, 
-  UDP_SPEED_DOWN        = 0x44, 
-  
-  UDP_GROUP_1_ON        = 0x45,
-  UDP_GROUP_1_OFF       = 0x46,
-  UDP_GROUP_2_ON        = 0x47,
-  UDP_GROUP_2_OFF       = 0x48,
-  UDP_GROUP_3_ON        = 0x49,
-  UDP_GROUP_3_OFF       = 0x4A,
-  UDP_GROUP_4_ON        = 0x4B,
-  UDP_GROUP_4_OFF       = 0x4C,
-  
-  UDP_DISCO_MODE        = 0x4D,
-  
-  UDP_GROUP_ALL_WHITE   = 0xC2,
-  UDP_GROUP_1_WHITE     = 0xC5,
-  UDP_GROUP_2_WHITE     = 0xC7,
-  UDP_GROUP_3_WHITE     = 0xC9,
-  UDP_GROUP_4_WHITE     = 0xCB,
+  UDP_CCT_GROUP_1_ON         = 0x38,
+  UDP_CCT_GROUP_1_OFF        = 0x3B,
+  UDP_CCT_GROUP_2_ON         = 0x3D,
+  UDP_CCT_GROUP_2_OFF        = 0x33,
+  UDP_CCT_GROUP_3_ON         = 0x37,
+  UDP_CCT_GROUP_3_OFF        = 0x3A,
+  UDP_CCT_GROUP_4_ON         = 0x32,
+  UDP_CCT_GROUP_4_OFF        = 0x36,
+  UDP_CCT_TEMPERATURE_DOWN   = 0x3F,
+  UDP_CCT_TEMPERATURE_UP     = 0x3E,
+  UDP_CCT_BRIGHTNESS_DOWN    = 0x34,
+  UDP_CCT_BRIGHTNESS_UP      = 0x3C,
   
-  UDP_BRIGHTNESS        = 0x4E,
-  UDP_COLOR             = 0x40
+  UDP_RGBW_ALL_ON            = 0x41,
+  UDP_RGBW_ALL_OFF           = 0x42,
+  UDP_RGBW_SPEED_UP          = 0x43, 
+  UDP_RGBW_SPEED_DOWN        = 0x44, 
+  UDP_RGBW_GROUP_1_ON        = 0x45,
+  UDP_RGBW_GROUP_1_OFF       = 0x46,
+  UDP_RGBW_GROUP_2_ON        = 0x47,
+  UDP_RGBW_GROUP_2_OFF       = 0x48,
+  UDP_RGBW_GROUP_3_ON        = 0x49,
+  UDP_RGBW_GROUP_3_OFF       = 0x4A,
+  UDP_RGBW_GROUP_4_ON        = 0x4B,
+  UDP_RGBW_GROUP_4_OFF       = 0x4C,
+  UDP_RGBW_DISCO_MODE        = 0x4D,
+  UDP_RGBW_GROUP_ALL_WHITE   = 0xC2,
+  UDP_RGBW_GROUP_1_WHITE     = 0xC5,
+  UDP_RGBW_GROUP_2_WHITE     = 0xC7,
+  UDP_RGBW_GROUP_3_WHITE     = 0xC9,
+  UDP_RGBW_GROUP_4_WHITE     = 0xCB,
+  UDP_RGBW_BRIGHTNESS        = 0x4E,
+  UDP_RGBW_COLOR             = 0x40
 };
 
 class MiLightUdpServer {
@@ -54,7 +64,9 @@ protected:
   char packetBuffer[MILIGHT_PACKET_BUFFER_SIZE];
   
   void handleCommand(uint8_t command, uint8_t commandArg);
-  void pressButton(uint8_t group, MiLightButton button);
+  void pressButton(uint8_t group, uint8_t button);
+  uint8_t cctCommandIdToGroup(uint8_t command);
+  MiLightStatus cctCommandToStatus(uint8_t command);
 };
 
 #endif

+ 10 - 0
lib/MiLight/PL1167_nRF24.cpp

@@ -145,6 +145,9 @@ int PL1167_nRF24::receive(uint8_t channel)
 
   _radio.startListening();
   if (_radio.available()) {
+#ifdef DEBUG_PRINTF
+  printf("Radio is available");
+#endif
     internal_receive();
   }
 
@@ -346,16 +349,23 @@ int PL1167_nRF24::internal_receive()
 
   if (_crc) {
     if (outp < 2) {
+#ifdef DEBUG_PRINTF
+  printf("Failed CRC: outp < 2\n");
+#endif
       return 0;
     }
     uint16_t crc = calc_crc(tmp, outp - 2);
     if ( ((crc & 0xff) != tmp[outp - 2]) || (((crc >> 8) & 0xff) != tmp[outp - 1]) ) {
+#ifdef DEBUG_PRINTF
+  printf("Failed CRC: expected %d, got (%d,%d)\n", crc, tmp[outp-2], tmp[outp-1]);
+#endif
       return 0;
     }
     outp -= 2;
   }
 
   memcpy(_packet, tmp, outp);
+  
   _packet_length = outp;
   _received = true;
   return outp;

+ 9 - 6
platformio.ini

@@ -8,13 +8,16 @@
 ; Please visit documentation for the other options and examples
 ; http://docs.platformio.org/page/projectconf.html
 
-[env:nodemcuv2]
-platform = espressif8266
-board = nodemcuv2
-framework = arduino
-lib_deps = 
-  RF24
+[common]
+lib_deps_builtin = 
   SPI
+lib_deps_external = 
+  RF24
   WiFiManager
   ArduinoJson
   Vector
+
+[env:nodemcuv2]
+platform = espressif8266
+board = nodemcuv2
+framework = arduino

+ 87 - 34
src/main.cpp

@@ -4,6 +4,7 @@
 #include <stdlib.h>
 #include <fs.h>
 #include <MiLightClient.h>
+#include <MiLightRadioConfig.h>
 #include <WebServer.h>
 #include <IntParsing.h>
 #include <Settings.h>
@@ -22,12 +23,21 @@ void handleUpdateGateway(const UrlTokenBindings* urlBindings) {
   JsonObject& request = buffer.parse(server->arg("plain"));
   
   const uint16_t deviceId = parseInt<uint16_t>(urlBindings->get("device_id"));
+  const MiLightRadioType type = MiLightClient::getRadioType(urlBindings->get("type"));
+  
+  if (type == UNKNOWN) {
+    String body = "Unknown device type: ";
+    body += urlBindings->get("type");
+    
+    server->send(400, "text/plain", body);
+    return;
+  }
   
   if (request.containsKey("status")) {
     if (request["status"] == "on") {
-      milightClient->allOn(deviceId);
+      milightClient->allOn(type, deviceId);
     } else if (request["status"] == "off") {
-      milightClient->allOff(deviceId);
+      milightClient->allOff(type, deviceId);
     }
   }
   
@@ -45,68 +55,111 @@ void handleUpdateGroup(const UrlTokenBindings* urlBindings) {
   
   const uint16_t deviceId = parseInt<uint16_t>(urlBindings->get("device_id"));
   const uint8_t groupId = urlBindings->get("group_id").toInt();
+  const MiLightRadioType type = MiLightClient::getRadioType(urlBindings->get("type"));
   
+  if (type == UNKNOWN) {
+    String body = "Unknown device type: ";
+    body += urlBindings->get("type");
+    
+    server->send(400, "text/plain", body);
+    return;
+  }
+    
   if (request.containsKey("status")) {
     const String& statusStr = request.get<String>("status");
     MiLightStatus status = (statusStr == "on" || statusStr == "true") ? ON : OFF;
-    milightClient->updateStatus(deviceId, groupId, status);
+    milightClient->updateStatus(type, deviceId, groupId, status);
   }
-  
-  if (request.containsKey("hue")) {
-    milightClient->updateHue(deviceId, groupId, request["hue"]);
-  }
-  
-  if (request.containsKey("level")) {
-    milightClient->updateBrightness(deviceId, groupId, request["level"]);
+      
+  if (request.containsKey("command")) {
+    if (request["command"] == "unpair") {
+      milightClient->unpair(type, deviceId, groupId);
+    }
+    
+    if (request["command"] == "pair") {
+      milightClient->pair(type, deviceId, groupId);
+    }
   }
   
-  if (request.containsKey("command")) {
-    if (request["command"] == "set_white") {
-      milightClient->updateColorWhite(deviceId, groupId);
+  if (type == RGBW) {
+    if (request.containsKey("hue")) {
+      milightClient->updateHue(deviceId, groupId, request["hue"]);
     }
     
-    if (request["command"] == "all_on") {
-      milightClient->allOn(deviceId);
+    if (request.containsKey("level")) {
+      milightClient->updateBrightness(deviceId, groupId, request["level"]);
     }
     
-    if (request["command"] == "all_off") {
-      milightClient->allOff(deviceId);
+    if (request.containsKey("command")) {
+      if (request["command"] == "set_white") {
+        milightClient->updateColorWhite(deviceId, groupId);
+      }
+    }
+  } else if (type == CCT) {
+    if (request.containsKey("temperature")) {
+      milightClient->updateTemperature(deviceId, groupId, request["temperature"]);
     }
     
-    if (request["command"] == "unpair") {
-      milightClient->unpair(deviceId, groupId);
+    if (request.containsKey("level")) {
+      milightClient->updateCctBrightness(deviceId, groupId, request["level"]);
     }
     
-    if (request["command"] == "pair") {
-      milightClient->pair(deviceId, groupId);
+    if (request.containsKey("command")) {
+      if (request["command"] == "level_up") {
+        milightClient->increaseCctBrightness(deviceId, groupId);
+      }
+      
+      if (request["command"] == "level_down") {
+        milightClient->decreaseCctBrightness(deviceId, groupId);
+      }
+      
+      if (request["command"] == "temperature_up") {
+        milightClient->increaseTemperature(deviceId, groupId);
+      }
+      
+      if (request["command"] == "temperature_down") {
+        milightClient->decreaseTemperature(deviceId, groupId);
+      }
     }
-  }
+  } 
   
   server->send(200, "application/json", "true");
 }
 
 void handleListenGateway() {
-  while (!milightClient->available()) {
+  uint8_t readType = 0;
+  MiLightRadioConfig *config;
+  
+  while (readType == 0) {
     if (!server->clientConnected()) {
       return;
     }
     
+    if (milightClient->available(RGBW)) {
+      readType = RGBW;
+      config = &MilightRgbwConfig;
+    } else if (milightClient->available(CCT)) {
+      readType = CCT;
+      config = &MilightCctConfig;
+    } else if (milightClient->available(RGBW_CCT)) {
+      readType = RGBW_CCT;
+      config = &MilightRgbwCctConfig;
+    }
+    
     yield();
   }
   
-  MiLightPacket packet;
-  milightClient->read(packet);
+  uint8_t packet[config->packetLength];
+  milightClient->read(static_cast<MiLightRadioType>(readType), packet);
   
   String response = "Packet received (";
   response += String(sizeof(packet)) + " bytes)";
   response += ":\n";
-  response += "Request type : " + String(packet.deviceType, HEX) + "\n";
-  response += "Device ID    : " + String(packet.deviceId, HEX) + "\n";
-  response += "Color        : " + String(packet.color, HEX) + "\n";
-  response += "Brightness   : " + String(packet.brightness, HEX) + "\n";
-  response += "Group ID     : " + String(packet.groupId, HEX) + "\n";
-  response += "Button       : " + String(packet.button, HEX) + "\n";
-  response += "Sequence Num : " + String(packet.sequenceNum, HEX) + "\n";
+  
+  for (int i = 0; i < sizeof(packet); i++) {
+    response += String(packet[i], HEX) + " ";
+  }
+  
   response += "\n\n";
   
   server->send(200, "text/plain", response);
@@ -230,8 +283,8 @@ void setup() {
   server->on("/settings", HTTP_GET, handleServeFile(SETTINGS_FILE, "application/json"));
   server->on("/settings", HTTP_PUT, handleUpdateSettings);
   server->on("/gateway_traffic", HTTP_GET, handleListenGateway);
-  server->onPattern("/gateways/:device_id/:group_id", HTTP_PUT, handleUpdateGroup);
-  server->onPattern("/gateways/:device_id", HTTP_PUT, handleUpdateGateway);
+  server->onPattern("/gateways/:device_id/:type/:group_id", HTTP_PUT, handleUpdateGroup);
+  server->onPattern("/gateways/:device_id/:type", HTTP_PUT, handleUpdateGateway);
   server->on("/web", HTTP_POST, onWebUpdated, handleUpdateFile(WEB_INDEX_FILENAME));
   server->on("/firmware", HTTP_POST, 
     [](){

+ 75 - 18
web/index.html

@@ -34,6 +34,7 @@
       background-color: #fff; 
       border: 1px solid #ccc;
     }
+    .inline { display: inline-block; }
     .hue-picker {
       height: 2em;
       width: 100%;
@@ -83,16 +84,21 @@
   
     var activeUrl = function() {
       var deviceId = $('#deviceId option:selected').val()
-        , groupId = $('#groupId input:checked').data('value');
+        , groupId = $('#groupId input:checked').data('value')
+        , mode = getCurrentMode();
         
       if (deviceId == "") {
         alert("Please enter a device ID.");
-        throw "abc";
+        throw "Must enter device ID";
       }
         
-      return "/gateways/" + deviceId + "/" + groupId;
+      return "/gateways/" + deviceId + "/" + mode + "/" + groupId;
     }
     
+    var getCurrentMode = function() {
+      return $('input[name="mode"]:checked').data('value');
+    };
+    
     var updateGroup = _.throttle(
       function(params) {
         $.ajax(
@@ -215,6 +221,18 @@
       }
     };
     
+    var updateModeOptions = function() {
+      var currentMode = getCurrentMode();
+      
+      $('.mode-option').map(function() {
+        if ($(this).data('for') != currentMode) {
+          $(this).hide();
+        } else {
+          $(this).show();
+        }
+      });
+    };
+    
     $(function() {
       $('.radio-option').click(function() {
         $(this).prev().prop('checked', true);
@@ -282,6 +300,8 @@
         $('#gateway-server-configs').append(gatewayServerRow('', ''));
       });
       
+      $('#mode').change(updateModeOptions);
+      
       $('body').on('click', '.remove-gateway-server', function() {
         $(this).closest('tr').remove();
       });
@@ -358,6 +378,7 @@
       });
       
       loadSettings();
+      updateModeOptions();
     });
   </script>
   
@@ -382,7 +403,7 @@
 				</select>
       </div>
       
-      <div class="col-sm-8">
+      <div class="col-sm-3">
         <label for="groupId">Group</label>
         
         <div class="btn-group" id="groupId" data-toggle="buttons">
@@ -403,25 +424,59 @@
           </label>
         </div>
       </div>
+      
+      <div class="col-sm-4">
+        <label for="groupId">Mode</label>
+        
+        <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>
+          <label class="btn btn-secondary">
+            <input type="radio" name="mode" autocomplete="off" data-value="cct"> CCT
+          </label>
+        </div>
+      </div>
     </div>
     
-    <div class="row">
-      <div class="col-sm-12">
-        <h5>Hue</h5>
+    <div class="row"><div class="col-sm-12">
+    <div class="mode-option" data-for="rgbw">
+      <div class="row">
+        <div class="col-sm-12">
+          <h5>Hue</h5>
+        </div>
+      </div>
+      
+      <div class="row">
+        <div class="col-sm-6">
+          <span class="hue-picker">
+            <span class="hue-picker-inner"></span>
+            <span class="hue-value-display"></span>
+          </span>
+        </div>
       </div>
     </div>
-    
-    <div class="row">
-      <div class="col-sm-6">
-        <span class="hue-picker">
-          <span class="hue-picker-inner"></span>
-          <span class="hue-value-display"></span>
-        </span>
+    </div></div>
+          
+    <div class="mode-option" data-for="cct">
+      <div class="row">
+        <div class="col-sm-12">
+          <h5>Color Temperature</h5>
+        </div>
+      </div>
+      <div class="row">
+        <div class="col-sm-6">
+          <input class="slider raw-update" name="temperature"
+              data-slider-min="0"
+              data-slider-max="10"
+              data-slider-value="10"
+          />
+        </div>
       </div>
     </div>
     
     <div class="row">
-      <div class="col-sm-6">
+      <div class="col-sm-12">
         <h5>Brightness</h5>
       </div>
     </div>
@@ -448,9 +503,11 @@
           <li>
             <input type="checkbox" name="status" class="raw-update" data-toggle="toggle" checked/>
           </li>
-          <li>
-            <button type="button" class="btn btn-secondary command-btn" data-command="set_white">White</button>
-          </li>
+          <div class="mode-option inline" data-for="rgbw">
+            <li>
+              <button type="button" class="btn btn-secondary command-btn" data-command="set_white">White</button>
+            </li>
+          </div>
           <li>
             <button type="button" class="btn btn-success command-btn" data-command="pair">
               <i class="glyphicon glyphicon-plus"></i>