浏览代码

Merge pull request #60 from sidoh/mqtt

Add MQTT support
Chris Mullins 8 年之前
父节点
当前提交
8447747e3e

+ 30 - 2
README.md

@@ -78,8 +78,8 @@ The HTTP endpoints (shown below) will be fully functional at this point. You sho
 1. `GET /settings`. Gets current settings as JSON.
 1. `GET /settings`. Gets current settings as JSON.
 1. `PUT /settings`. Patches settings (e.g., doesn't overwrite keys that aren't present). Accepts a JSON blob in the body.
 1. `PUT /settings`. Patches settings (e.g., doesn't overwrite keys that aren't present). Accepts a JSON blob in the body.
 1. `GET /radio_configs`. Get a list of supported radio configs (aka `device_type`s).
 1. `GET /radio_configs`. Get a list of supported radio configs (aka `device_type`s).
-1. `GET /gateway_traffic/:device_type`. Starts an HTTP long poll. Returns any Milight traffic it hears. Useful if you need to know what your Milight gateway/remote ID is. Since protocols for RGBW/CCT are different, specify one of `rgbw`, `cct`, or `rgb_cct` as `:device_type. Accepts a JSON blob.
-1. `PUT /gateways/:device_id/:device_type/:group_id`. Controls or sends commands to `:group_id` from `:device_id`.
+1. `GET /gateway_traffic/:device_type`. Starts an HTTP long poll. Returns any Milight traffic it hears. Useful if you need to know what your Milight gateway/remote ID is. Since protocols for RGBW/CCT are different, specify one of `rgbw`, `cct`, or `rgb_cct` as `:device_type.
+1. `PUT /gateways/:device_id/:device_type/:group_id`. Controls or sends commands to `:group_id` from `:device_id`. Accepts a JSON blob. The schema is documented below in the _Bulb commands_ section.
 1. `POST /raw_commands/:device_type`. Sends a raw RF packet with radio configs associated with `:device_type`. Example body:
 1. `POST /raw_commands/:device_type`. Sends a raw RF packet with radio configs associated with `:device_type`. Example body:
     ```
     ```
     {"packet": "01 02 03 04 05 06 07 08 09", "num_repeats": 10}
     {"packet": "01 02 03 04 05 06 07 08 09", "num_repeats": 10}
@@ -127,6 +127,34 @@ $ curl --data-binary '{"command":"set_white"}' -X PUT http://esp8266/gateways/0x
 true%
 true%
 ```
 ```
 
 
+## MQTT
+
+To configure your ESP to integrate with MQTT, fill out the following settings:
+
+1. `mqtt_server`- IP or hostname should work. Specify a port with standard syntax (e.g., "mymqttbroker.com:1884").
+1. `mqtt_topic_pattern` - you can control arbitrary configurations of device ID, device type, and group ID with this. A good default choice is something like `milight/:device_id/:device_type/:group_id`. More detail is provided below.
+1. (optionally) `mqtt_username`
+1. (optionally) `mqtt_password`
+
+#### More detail on `mqtt_topic_pattern`
+
+`mqtt_topic_pattern` leverages single-level wildcards (documented [here](https://mosquitto.org/man/mqtt-7.html)). For example, specifying `milight/:device_id/:device_type/:group_id` will cause the ESP to subscribe to the topic `milight/+/+/+`. It will then interpret the second, third, and fourth tokens in topics it receives messages on as `:device_id`, `:device_type`, and `:group_id`, respectively.
+
+Messages should be JSON objects using exactly the same schema that the REST gateway uses for the `/gateways/:device_id/:device_type/:group_id` endpoint. Documented above in the _Bulb commands_ section.
+
+##### Example:
+
+If `mqtt_topic_pattern` is set to `milight/:device_id/:device_type/:group_id`, you could send the following message to it (the below example uses a ruby MQTT client):
+
+```ruby
+irb(main):001:0> require 'mqtt'
+irb(main):002:0> client = MQTT::Client.new('10.133.8.11',1883)
+irb(main):003:0> client.connect
+irb(main):004:0> client.publish('milight/0x118D/rgb_cct/1', '{"status":"ON","color":{"r":255,"g":200,"b":255},"brightness":100}')
+```
+
+This will instruct the ESP to send messages to RGB+CCT bulbs with device ID `0x118D` in group 1 to turn on, set color to RGB(255,200,255), and brightness to 100.
+
 ## UDP Gateways
 ## UDP Gateways
 
 
 You can add an arbitrary number of UDP gateways through the REST API or through the web UI. Each gateway server listens on a port and responds to the standard set of commands supported by the Milight protocol. This should allow you to use one of these with standard Milight integrations (SmartThings, Home Assistant, OpenHAB, etc.).
 You can add an arbitrary number of UDP gateways through the REST API or through the web UI. Each gateway server listens on a port and responds to the standard set of commands supported by the Milight protocol. This should allow you to use one of these with standard Milight integrations (SmartThings, Home Assistant, OpenHAB, etc.).

+ 7 - 2
data/web/index.html

@@ -124,7 +124,8 @@
   <script lang="text/javascript">
   <script lang="text/javascript">
     var FORM_SETTINGS = [
     var FORM_SETTINGS = [
       "admin_username", "admin_password", "ce_pin", "csn_pin", "reset_pin","packet_repeats",
       "admin_username", "admin_password", "ce_pin", "csn_pin", "reset_pin","packet_repeats",
-      "http_repeat_factor", "auto_restart_period", "radio_interface_type"
+      "http_repeat_factor", "auto_restart_period", "mqtt_server", "mqtt_topic_pattern",
+      "mqtt_username", "mqtt_password", "radio_interface_type"
     ];
     ];
 
 
     var FORM_SETTINGS_HELP = {
     var FORM_SETTINGS_HELP = {
@@ -136,7 +137,11 @@
       auto_restart_period : "Automatically restart the device every number of " +
       auto_restart_period : "Automatically restart the device every number of " +
         "minutes specified. Use 0 for disabled.",
         "minutes specified. Use 0 for disabled.",
       radio_interface_type : "2.4 GHz radio model. Only change this if you know " +
       radio_interface_type : "2.4 GHz radio model. Only change this if you know " +
-        "You're not using an NRF24L01!"
+        "You're not using an NRF24L01!",
+      mqtt_server : "Domain or IP address of MQTT broker. Optionally specify a port " +
+        "with (example) mymqqtbroker.com:1884.",
+      mqtt_topic_pattern : "Pattern for MQTT topics to listen on. Example: " +
+        "lights/:device_id/:type/:group. See README for further details."
     }
     }
 
 
     var UDP_PROTOCOL_VERSIONS = [ 5, 6 ];
     var UDP_PROTOCOL_VERSIONS = [ 5, 6 ];

lib/WebServer/TokenIterator.cpp → lib/Helpers/TokenIterator.cpp


+ 2 - 1
lib/WebServer/TokenIterator.h

@@ -1,7 +1,8 @@
 #include <Arduino.h>
 #include <Arduino.h>
 
 
 #ifndef _TOKEN_ITERATOR_H
 #ifndef _TOKEN_ITERATOR_H
-#define _TOKEN_ITERATOR_H value
+#define _TOKEN_ITERATOR_H
+
 class TokenIterator {
 class TokenIterator {
 public:
 public:
   TokenIterator(char* data, size_t length, char sep = ',');
   TokenIterator(char* data, size_t length, char sep = ',');

+ 35 - 0
lib/Helpers/UrlTokenBindings.cpp

@@ -0,0 +1,35 @@
+#include <UrlTokenBindings.h>
+
+UrlTokenBindings::UrlTokenBindings(TokenIterator& patternTokens, TokenIterator& requestTokens)
+  : patternTokens(patternTokens),
+    requestTokens(requestTokens)
+{ }
+
+bool UrlTokenBindings::hasBinding(const char* searchToken) const {
+  patternTokens.reset();
+  while (patternTokens.hasNext()) {
+    const char* token = patternTokens.nextToken();
+
+    if (token[0] == ':' && strcmp(token+1, searchToken) == 0) {
+      return true;
+    }
+  }
+
+  return false;
+}
+
+const char* UrlTokenBindings::get(const char* searchToken) const {
+  patternTokens.reset();
+  requestTokens.reset();
+
+  while (patternTokens.hasNext() && requestTokens.hasNext()) {
+    const char* token = patternTokens.nextToken();
+    const char* binding = requestTokens.nextToken();
+
+    if (token[0] == ':' && strcmp(token+1, searchToken) == 0) {
+      return binding;
+    }
+  }
+
+  return NULL;
+}

+ 18 - 0
lib/Helpers/UrlTokenBindings.h

@@ -0,0 +1,18 @@
+#include <TokenIterator.h>
+
+#ifndef _URL_TOKEN_BINDINGS_H
+#define _URL_TOKEN_BINDINGS_H
+
+class UrlTokenBindings {
+public:
+  UrlTokenBindings(TokenIterator& patternTokens, TokenIterator& requestTokens);
+
+  bool hasBinding(const char* key) const;
+  const char* get(const char* key) const;
+
+private:
+  TokenIterator& patternTokens;
+  TokenIterator& requestTokens;
+};
+
+#endif

+ 127 - 0
lib/MQTT/MqttClient.cpp

@@ -0,0 +1,127 @@
+#include <MqttClient.h>
+#include <TokenIterator.h>
+#include <UrlTokenBindings.h>
+#include <IntParsing.h>
+#include <ArduinoJson.h>
+#include <WiFiClient.h>
+
+MqttClient::MqttClient(Settings& settings, MiLightClient*& milightClient)
+  : milightClient(milightClient),
+    settings(settings)
+{
+  String strDomain = settings.mqttServer();
+  this->domain = new char[strDomain.length() + 1];
+  strcpy(this->domain, strDomain.c_str());
+
+  this->mqttClient = new PubSubClient(tcpClient);
+}
+
+MqttClient::~MqttClient() {
+  mqttClient->disconnect();
+  delete this->domain;
+}
+
+void MqttClient::begin() {
+#ifdef MQTT_DEBUG
+  printf_P(
+    PSTR("MqttClient - Connecting to: %s\nparsed:%s:%u\n"),
+    settings._mqttServer.c_str(),
+    settings.mqttServer().c_str(),
+    settings.mqttPort()
+  );
+#endif
+
+  mqttClient->setServer(this->domain, settings.mqttPort());
+  mqttClient->setCallback(
+    [this](char* topic, byte* payload, int length) {
+      this->publishCallback(topic, payload, length);
+    }
+  );
+  reconnect();
+}
+
+bool MqttClient::connect() {
+  char nameBuffer[30];
+  sprintf_P(nameBuffer, PSTR("milight-hub-%u"), ESP.getChipId());
+
+#ifdef MQTT_DEBUG
+    Serial.println(F("MqttClient - connecting"));
+#endif
+
+  if (settings.mqttUsername.length() > 0) {
+    return mqttClient->connect(
+      nameBuffer,
+      settings.mqttUsername.c_str(),
+      settings.mqttPassword.c_str()
+    );
+  } else {
+    return mqttClient->connect(nameBuffer);
+  }
+}
+
+void MqttClient::reconnect() {
+  if (! mqttClient->connected()) {
+    if (connect()) {
+      subscribe();
+    } else {
+      Serial.println(F("ERROR: Failed to connect to MQTT server"));
+    }
+  }
+}
+
+void MqttClient::handleClient() {
+  reconnect();
+  mqttClient->loop();
+}
+
+void MqttClient::subscribe() {
+  String topic = settings.mqttTopicPattern;
+
+  topic.replace(":device_id", "+");
+  topic.replace(":group_id", "+");
+  topic.replace(":device_type", "+");
+
+  mqttClient->subscribe(topic.c_str());
+}
+
+void MqttClient::publishCallback(char* topic, byte* payload, int length) {
+  uint16_t deviceId = 0;
+  uint8_t groupId = 0;
+  MiLightRadioConfig* config = &MilightRgbCctConfig;
+  char cstrPayload[length + 1];
+  cstrPayload[length] = 0;
+  memcpy(cstrPayload, payload, sizeof(byte)*length);
+
+#ifdef MQTT_DEBUG
+  printf_P(PSTR("MqttClient - Got message on topic: %s\n%s\n"), topic, cstrPayload);
+#endif
+
+  char topicPattern[settings.mqttTopicPattern.length()];
+  strcpy(topicPattern, settings.mqttTopicPattern.c_str());
+
+  TokenIterator patternIterator(topicPattern, settings.mqttTopicPattern.length(), '/');
+  TokenIterator topicIterator(topic, strlen(topic), '/');
+  UrlTokenBindings tokenBindings(patternIterator, topicIterator);
+
+  if (tokenBindings.hasBinding("device_id")) {
+    deviceId = parseInt<uint16_t>(tokenBindings.get("device_id"));
+  }
+
+  if (tokenBindings.hasBinding("group_id")) {
+    groupId = parseInt<uint16_t>(tokenBindings.get("group_id"));
+  }
+
+  if (tokenBindings.hasBinding("device_type")) {
+    config = MiLightRadioConfig::fromString(tokenBindings.get("device_type"));
+  }
+
+  StaticJsonBuffer<400> buffer;
+  JsonObject& obj = buffer.parseObject(cstrPayload);
+
+#ifdef MQTT_DEBUG
+  printf_P(PSTR("MqttClient - device %04X, group %u\n"), deviceId, groupId);
+#endif
+
+  milightClient->prepare(*config, deviceId, groupId);
+  milightClient->update(obj);
+}

+ 30 - 0
lib/MQTT/MqttClient.h

@@ -0,0 +1,30 @@
+#include <MiLightClient.h>
+#include <Settings.h>
+#include <PubSubClient.h>
+#include <WiFiClient.h>
+
+#ifndef _MQTT_CLIENT_H
+#define _MQTT_CLIENT_H
+
+class MqttClient {
+public:
+  MqttClient(Settings& settings, MiLightClient*& milightClient);
+  ~MqttClient();
+
+  void begin();
+  void handleClient();
+  void reconnect();
+
+private:
+  WiFiClient tcpClient;
+  PubSubClient* mqttClient;
+  MiLightClient*& milightClient;
+  Settings& settings;
+  char* domain;
+
+  bool connect();
+  void subscribe();
+  void publishCallback(char* topic, byte* payload, int length);
+};
+
+#endif

+ 129 - 0
lib/MiLight/MiLightClient.cpp

@@ -1,6 +1,10 @@
 #include <MiLightClient.h>
 #include <MiLightClient.h>
 #include <MiLightRadioConfig.h>
 #include <MiLightRadioConfig.h>
 #include <Arduino.h>
 #include <Arduino.h>
+#include <RGBConverter.h>
+
+#define COLOR_TEMP_MAX_MIREDS 370
+#define COLOR_TEMP_MIN_MIREDS 153
 
 
 MiLightClient::MiLightClient(MiLightRadioFactory* radioFactory)
 MiLightClient::MiLightClient(MiLightRadioFactory* radioFactory)
   : resendCount(MILIGHT_DEFAULT_RESEND_COUNT),
   : resendCount(MILIGHT_DEFAULT_RESEND_COUNT),
@@ -217,6 +221,131 @@ void MiLightClient::command(uint8_t command, uint8_t arg) {
   flushPacket();
   flushPacket();
 }
 }
 
 
+void MiLightClient::update(const JsonObject& request) {
+  if (request.containsKey("status") || request.containsKey("state")) {
+    String strStatus;
+
+    if (request.containsKey("status")) {
+      strStatus = request.get<char*>("status");
+    } else {
+      strStatus = request.get<char*>("state");
+    }
+
+    MiLightStatus status = (strStatus.equalsIgnoreCase("on") || strStatus.equalsIgnoreCase("true")) ? ON : OFF;
+    this->updateStatus(status);
+  }
+
+  if (request.containsKey("command")) {
+    if (request["command"] == "unpair") {
+      this->unpair();
+    }
+
+    if (request["command"] == "pair") {
+      this->pair();
+    }
+
+    if (request["command"] == "set_white") {
+      this->updateColorWhite();
+    }
+
+    if (request["command"] == "night_mode") {
+      this->enableNightMode();
+    }
+
+    if (request["command"] == "level_up") {
+      this->increaseBrightness();
+    }
+
+    if (request["command"] == "level_down") {
+      this->decreaseBrightness();
+    }
+
+    if (request["command"] == "temperature_up") {
+      this->increaseTemperature();
+    }
+
+    if (request["command"] == "temperature_down") {
+      this->decreaseTemperature();
+    }
+
+    if (request["command"] == "next_mode") {
+      this->nextMode();
+    }
+
+    if (request["command"] == "previous_mode") {
+      this->previousMode();
+    }
+
+    if (request["command"] == "mode_speed_down") {
+      this->modeSpeedDown();
+    }
+
+    if (request["command"] == "mode_speed_up") {
+      this->modeSpeedUp();
+    }
+  }
+
+  if (request.containsKey("hue")) {
+    this->updateHue(request["hue"]);
+  }
+  if (request.containsKey("saturation")) {
+    this->updateSaturation(request["saturation"]);
+  }
+
+  // Convert RGB to HSV
+  if (request.containsKey("color")) {
+    JsonObject& color = request["color"];
+
+    uint8_t r = color["r"];
+    uint8_t g = color["g"];
+    uint8_t b = color["b"];
+
+    double hsv[3];
+    RGBConverter converter;
+    converter.rgbToHsv(r, g, b, hsv);
+
+    uint16_t hue = round(hsv[0]*360);
+    uint8_t saturation = round(hsv[1]*100);
+
+    this->updateHue(hue);
+    this->updateSaturation(saturation);
+  }
+
+  if (request.containsKey("level")) {
+    this->updateBrightness(request["level"]);
+  }
+  // HomeAssistant
+  if (request.containsKey("brightness")) {
+    uint8_t scaledBrightness = round(request.get<uint8_t>("brightness") * (100/255.0));
+    this->updateBrightness(scaledBrightness);
+  }
+
+  if (request.containsKey("temperature")) {
+    this->updateTemperature(request["temperature"]);
+  }
+  // HomeAssistant
+  if (request.containsKey("color_temp")) {
+    // MiLight CCT bulbs range from 2700K-6500K, or ~370.3-153.8 mireds. Note
+    // that mireds are inversely correlated with color temperature.
+    uint32_t tempMireds = request["color_temp"];
+    tempMireds = tempMireds > COLOR_TEMP_MAX_MIREDS ? COLOR_TEMP_MAX_MIREDS : tempMireds;
+    tempMireds = tempMireds < COLOR_TEMP_MIN_MIREDS ? COLOR_TEMP_MIN_MIREDS : tempMireds;
+
+    uint8_t scaledTemp = round(
+      100*
+      (tempMireds - COLOR_TEMP_MIN_MIREDS)
+        /
+      static_cast<double>(COLOR_TEMP_MAX_MIREDS - COLOR_TEMP_MIN_MIREDS)
+    );
+
+    this->updateTemperature(100 - scaledTemp);
+  }
+
+  if (request.containsKey("mode")) {
+    this->updateMode(request["mode"]);
+  }
+}
+
 void MiLightClient::formatPacket(uint8_t* packet, char* buffer) {
 void MiLightClient::formatPacket(uint8_t* packet, char* buffer) {
   formatter->format(packet, buffer);
   formatter->format(packet, buffer);
 }
 }

+ 2 - 0
lib/MiLight/MiLightClient.h

@@ -59,6 +59,8 @@ public:
 
 
   void formatPacket(uint8_t* packet, char* buffer);
   void formatPacket(uint8_t* packet, char* buffer);
 
 
+  void update(const JsonObject& object);
+
 
 
 protected:
 protected:
 
 

+ 42 - 72
lib/Settings/Settings.cpp

@@ -4,6 +4,8 @@
 #include <IntParsing.h>
 #include <IntParsing.h>
 #include <algorithm>
 #include <algorithm>
 
 
+#define PORT_POSITION(s) ( s.indexOf(':') )
+
 bool Settings::hasAuthSettings() {
 bool Settings::hasAuthSettings() {
   return adminUsername.length() > 0 && adminPassword.length() > 0;
   return adminUsername.length() > 0 && adminPassword.length() > 0;
 }
 }
@@ -23,53 +25,7 @@ size_t Settings::getAutoRestartPeriod() {
 void Settings::deserialize(Settings& settings, String json) {
 void Settings::deserialize(Settings& settings, String json) {
   DynamicJsonBuffer jsonBuffer;
   DynamicJsonBuffer jsonBuffer;
   JsonObject& parsedSettings = jsonBuffer.parseObject(json);
   JsonObject& parsedSettings = jsonBuffer.parseObject(json);
-  deserialize(settings, parsedSettings);
-}
-
-void Settings::deserialize(Settings& settings, JsonObject& parsedSettings) {
-  if (parsedSettings.success()) {
-    if (parsedSettings.containsKey("admin_username")) {
-      settings.adminUsername = parsedSettings.get<String>("admin_username");
-    }
-
-    if (parsedSettings.containsKey("admin_password")) {
-      settings.adminPassword = parsedSettings.get<String>("admin_password");
-    }
-
-    if (parsedSettings.containsKey("ce_pin")) {
-      settings.cePin = parsedSettings["ce_pin"];
-    }
-
-    if (parsedSettings.containsKey("csn_pin")) {
-      settings.csnPin = parsedSettings["csn_pin"];
-    }
-
-    if (parsedSettings.containsKey("reset_pin")) {
-      settings.resetPin = parsedSettings["reset_pin"];
-    }
-
-    if (parsedSettings.containsKey("radio_interface_type")) {
-      settings.radioInterfaceType = typeFromString(parsedSettings["radio_interface_type"]);
-    }
-
-    if (parsedSettings.containsKey("packet_repeats")) {
-      settings.packetRepeats = parsedSettings["packet_repeats"];
-    }
-
-    if (parsedSettings.containsKey("http_repeat_factor")) {
-      settings.httpRepeatFactor = parsedSettings["http_repeat_factor"];
-    }
-
-    if (parsedSettings.containsKey("auto_restart_period")) {
-      settings._autoRestartPeriod = parsedSettings["auto_restart_period"];
-    }
-
-    JsonArray& arr = parsedSettings["device_ids"];
-    settings.updateDeviceIds(arr);
-
-    JsonArray& gatewayArr = parsedSettings["gateway_configs"];
-    settings.updateGatewayConfigs(gatewayArr);
-  }
+  settings.patch(parsedSettings);
 }
 }
 
 
 void Settings::updateDeviceIds(JsonArray& arr) {
 void Settings::updateDeviceIds(JsonArray& arr) {
@@ -108,33 +64,23 @@ void Settings::updateGatewayConfigs(JsonArray& arr) {
 
 
 void Settings::patch(JsonObject& parsedSettings) {
 void Settings::patch(JsonObject& parsedSettings) {
   if (parsedSettings.success()) {
   if (parsedSettings.success()) {
-    if (parsedSettings.containsKey("admin_username")) {
-      this->adminUsername = parsedSettings.get<String>("admin_username");
-    }
-    if (parsedSettings.containsKey("admin_password")) {
-      this->adminPassword = parsedSettings.get<String>("admin_password");
-    }
-    if (parsedSettings.containsKey("ce_pin")) {
-      this->cePin = parsedSettings["ce_pin"];
-    }
-    if (parsedSettings.containsKey("csn_pin")) {
-      this->csnPin = parsedSettings["csn_pin"];
-    }
-    if (parsedSettings.containsKey("reset_pin")) {
-      this->resetPin = parsedSettings["reset_pin"];
-    }
+    this->setIfPresent<String>(parsedSettings, "admin_username", adminUsername);
+    this->setIfPresent(parsedSettings, "admin_password", adminPassword);
+    this->setIfPresent(parsedSettings, "ce_pin", cePin);
+    this->setIfPresent(parsedSettings, "csn_pin", csnPin);
+    this->setIfPresent(parsedSettings, "reset_pin", resetPin);
+    this->setIfPresent(parsedSettings, "packet_repeats", packetRepeats);
+    this->setIfPresent(parsedSettings, "http_repeat_factor", httpRepeatFactor);
+    this->setIfPresent(parsedSettings, "auto_restart_period", _autoRestartPeriod);
+    this->setIfPresent(parsedSettings, "mqtt_server", _mqttServer);
+    this->setIfPresent(parsedSettings, "mqtt_username", mqttUsername);
+    this->setIfPresent(parsedSettings, "mqtt_password", mqttPassword);
+    this->setIfPresent(parsedSettings, "mqtt_topic_pattern", mqttTopicPattern);
+
     if (parsedSettings.containsKey("radio_interface_type")) {
     if (parsedSettings.containsKey("radio_interface_type")) {
-      this->radioInterfaceType = typeFromString(parsedSettings["radio_interface_type"]);
-    }
-    if (parsedSettings.containsKey("packet_repeats")) {
-      this->packetRepeats = parsedSettings["packet_repeats"];
-    }
-    if (parsedSettings.containsKey("http_repeat_factor")) {
-      this->httpRepeatFactor = parsedSettings["http_repeat_factor"];
-    }
-    if (parsedSettings.containsKey("auto_restart_period")) {
-      this->_autoRestartPeriod = parsedSettings["auto_restart_period"];
+      this->radioInterfaceType = Settings::typeFromString(parsedSettings["radio_interface_type"]);
     }
     }
+
     if (parsedSettings.containsKey("device_ids")) {
     if (parsedSettings.containsKey("device_ids")) {
       JsonArray& arr = parsedSettings["device_ids"];
       JsonArray& arr = parsedSettings["device_ids"];
       updateDeviceIds(arr);
       updateDeviceIds(arr);
@@ -189,6 +135,10 @@ void Settings::serialize(Stream& stream, const bool prettyPrint) {
   root["packet_repeats"] = this->packetRepeats;
   root["packet_repeats"] = this->packetRepeats;
   root["http_repeat_factor"] = this->httpRepeatFactor;
   root["http_repeat_factor"] = this->httpRepeatFactor;
   root["auto_restart_period"] = this->_autoRestartPeriod;
   root["auto_restart_period"] = this->_autoRestartPeriod;
+  root["mqtt_server"] = this->_mqttServer;
+  root["mqtt_username"] = this->mqttUsername;
+  root["mqtt_password"] = this->mqttPassword;
+  root["mqtt_topic_pattern"] = this->mqttTopicPattern;
 
 
   if (this->deviceIds) {
   if (this->deviceIds) {
     JsonArray& arr = jsonBuffer.createArray();
     JsonArray& arr = jsonBuffer.createArray();
@@ -216,6 +166,26 @@ void Settings::serialize(Stream& stream, const bool prettyPrint) {
   }
   }
 }
 }
 
 
+String Settings::mqttServer() {
+  int pos = PORT_POSITION(_mqttServer);
+
+  if (pos == -1) {
+    return _mqttServer;
+  } else {
+    return _mqttServer.substring(0, pos);
+  }
+}
+
+uint16_t Settings::mqttPort() {
+  int pos = PORT_POSITION(_mqttServer);
+
+  if (pos == -1) {
+    return DEFAULT_MQTT_PORT;
+  } else {
+    return atoi(_mqttServer.c_str() + pos + 1);
+  }
+}
+
 RadioInterfaceType Settings::typeFromString(const String& s) {
 RadioInterfaceType Settings::typeFromString(const String& s) {
   if (s.equalsIgnoreCase("lt8900")) {
   if (s.equalsIgnoreCase("lt8900")) {
     return LT8900;
     return LT8900;

+ 14 - 1
lib/Settings/Settings.h

@@ -26,6 +26,7 @@
 #define MILIGHT_REPO_WEB_PATH "/data/web/index.html"
 #define MILIGHT_REPO_WEB_PATH "/data/web/index.html"
 
 
 #define MINIMUM_RESTART_PERIOD 1
 #define MINIMUM_RESTART_PERIOD 1
+#define DEFAULT_MQTT_PORT 1883
 
 
 enum RadioInterfaceType {
 enum RadioInterfaceType {
   nRF24 = 0,
   nRF24 = 0,
@@ -75,7 +76,6 @@ public:
   size_t getAutoRestartPeriod();
   size_t getAutoRestartPeriod();
 
 
   static void deserialize(Settings& settings, String json);
   static void deserialize(Settings& settings, String json);
-  static void deserialize(Settings& settings, JsonObject& json);
   static void load(Settings& settings);
   static void load(Settings& settings);
 
 
   static RadioInterfaceType typeFromString(const String& s);
   static RadioInterfaceType typeFromString(const String& s);
@@ -87,6 +87,8 @@ public:
   void updateDeviceIds(JsonArray& arr);
   void updateDeviceIds(JsonArray& arr);
   void updateGatewayConfigs(JsonArray& arr);
   void updateGatewayConfigs(JsonArray& arr);
   void patch(JsonObject& obj);
   void patch(JsonObject& obj);
+  String mqttServer();
+  uint16_t mqttPort();
 
 
   String adminUsername;
   String adminUsername;
   String adminPassword;
   String adminPassword;
@@ -100,9 +102,20 @@ public:
   size_t numDeviceIds;
   size_t numDeviceIds;
   size_t packetRepeats;
   size_t packetRepeats;
   size_t httpRepeatFactor;
   size_t httpRepeatFactor;
+  String _mqttServer;
+  String mqttUsername;
+  String mqttPassword;
+  String mqttTopicPattern;
 
 
 protected:
 protected:
   size_t _autoRestartPeriod;
   size_t _autoRestartPeriod;
+
+  template <typename T>
+  void setIfPresent(JsonObject& obj, const char* key, T& var) {
+    if (obj.containsKey(key)) {
+      var = obj.get<T>(key);
+    }
+  }
 };
 };
 
 
 #endif
 #endif

+ 1 - 77
lib/WebServer/MiLightHttpServer.cpp

@@ -373,83 +373,7 @@ void MiLightHttpServer::handleUpdateGroup(const UrlTokenBindings* urlBindings) {
 }
 }
 
 
 void MiLightHttpServer::handleRequest(const JsonObject& request) {
 void MiLightHttpServer::handleRequest(const JsonObject& request) {
-  if (request.containsKey("status")) {
-    const String& statusStr = request.get<String>("status");
-    MiLightStatus status = (statusStr == "on" || statusStr == "true") ? ON : OFF;
-    milightClient->updateStatus(status);
-  }
-
-  if (request.containsKey("command")) {
-    if (request["command"] == "unpair") {
-      milightClient->unpair();
-    }
-
-    if (request["command"] == "pair") {
-      milightClient->pair();
-    }
-
-    if (request["command"] == "set_white") {
-      milightClient->updateColorWhite();
-    }
-
-    if (request["command"] == "night_mode") {
-      milightClient->enableNightMode();
-    }
-
-    if (request["command"] == "level_up") {
-      milightClient->increaseBrightness();
-    }
-
-    if (request["command"] == "level_down") {
-      milightClient->decreaseBrightness();
-    }
-
-    if (request["command"] == "temperature_up") {
-      milightClient->increaseTemperature();
-    }
-
-    if (request["command"] == "temperature_down") {
-      milightClient->decreaseTemperature();
-    }
-
-    if (request["command"] == "next_mode") {
-      milightClient->nextMode();
-    }
-
-    if (request["command"] == "previous_mode") {
-      milightClient->previousMode();
-    }
-
-    if (request["command"] == "mode_speed_down") {
-      milightClient->modeSpeedDown();
-    }
-
-    if (request["command"] == "mode_speed_up") {
-      milightClient->modeSpeedUp();
-    }
-  }
-
-  if (request.containsKey("hue")) {
-    milightClient->updateHue(request["hue"]);
-  }
-
-  if (request.containsKey("level")) {
-    milightClient->updateBrightness(request["level"]);
-  }
-
-  if (request.containsKey("temperature")) {
-    milightClient->updateTemperature(request["temperature"]);
-  }
-
-  if (request.containsKey("saturation")) {
-    milightClient->updateSaturation(request["saturation"]);
-  }
-
-  if (request.containsKey("mode")) {
-    milightClient->updateMode(request["mode"]);
-  }
-
-  milightClient->setResendCount(settings.packetRepeats);
+  milightClient->update(request);
 }
 }
 
 
 void MiLightHttpServer::handleSendRaw(const UrlTokenBindings* bindings) {
 void MiLightHttpServer::handleSendRaw(const UrlTokenBindings* bindings) {

+ 0 - 34
lib/WebServer/PatternHandler.cpp

@@ -1,39 +1,5 @@
 #include <PatternHandler.h>
 #include <PatternHandler.h>
 
 
-UrlTokenBindings::UrlTokenBindings(TokenIterator& patternTokens, TokenIterator& requestTokens)
-  : patternTokens(patternTokens),
-    requestTokens(requestTokens)
-{ }
-
-bool UrlTokenBindings::hasBinding(const char* searchToken) const {
-  patternTokens.reset();
-  while (patternTokens.hasNext()) {
-    const char* token = patternTokens.nextToken();
-
-    if (strcmp(token, searchToken) == 0) {
-      return true;
-    }
-  }
-
-  return false;
-}
-
-const char* UrlTokenBindings::get(const char* searchToken) const {
-  patternTokens.reset();
-  requestTokens.reset();
-
-  while (patternTokens.hasNext() && requestTokens.hasNext()) {
-    const char* token = patternTokens.nextToken();
-    const char* binding = requestTokens.nextToken();
-
-    if (token[0] == ':' && strcmp(token+1, searchToken) == 0) {
-      return binding;
-    }
-  }
-
-  return NULL;
-}
-
 PatternHandler::PatternHandler(
 PatternHandler::PatternHandler(
     const String& pattern,
     const String& pattern,
     const HTTPMethod method,
     const HTTPMethod method,

+ 1 - 12
lib/WebServer/PatternHandler.h

@@ -5,18 +5,7 @@
 #include <ESP8266WebServer.h>
 #include <ESP8266WebServer.h>
 #include <functional>
 #include <functional>
 #include <TokenIterator.h>
 #include <TokenIterator.h>
-
-class UrlTokenBindings {
-public:
-  UrlTokenBindings(TokenIterator& patternTokens, TokenIterator& requestTokens);
-
-  bool hasBinding(const char* key) const;
-  const char* get(const char* key) const;
-
-private:
-  TokenIterator& patternTokens;
-  TokenIterator& requestTokens;
-};
+#include <UrlTokenBindings.h>
 
 
 class PatternHandler : public RequestHandler {
 class PatternHandler : public RequestHandler {
 public:
 public:

+ 3 - 0
platformio.ini

@@ -16,7 +16,10 @@ lib_deps_external =
   RF24
   RF24
   WiFiManager
   WiFiManager
   ArduinoJson
   ArduinoJson
+  PubSubClient
+  https://github.com/ratkins/RGBConverter
 build_flags = !python .get_version.py
 build_flags = !python .get_version.py
+# -D MQTT_DEBUG
 # -D MILIGHT_UDP_DEBUG
 # -D MILIGHT_UDP_DEBUG
 # -D DEBUG_PRINTF
 # -D DEBUG_PRINTF
 
 

+ 15 - 0
src/main.cpp

@@ -13,6 +13,8 @@
 #include <MiLightUdpServer.h>
 #include <MiLightUdpServer.h>
 #include <ESP8266mDNS.h>
 #include <ESP8266mDNS.h>
 #include <ESP8266SSDP.h>
 #include <ESP8266SSDP.h>
+#include <MqttClient.h>
+#include <RGBConverter.h>
 
 
 WiFiManager wifiManager;
 WiFiManager wifiManager;
 
 
@@ -21,6 +23,7 @@ Settings settings;
 MiLightClient* milightClient;
 MiLightClient* milightClient;
 MiLightRadioFactory* radioFactory;
 MiLightRadioFactory* radioFactory;
 MiLightHttpServer *httpServer;
 MiLightHttpServer *httpServer;
+MqttClient* mqttClient;
 
 
 int numUdpServers = 0;
 int numUdpServers = 0;
 MiLightUdpServer** udpServers;
 MiLightUdpServer** udpServers;
@@ -66,6 +69,9 @@ void applySettings() {
   if (radioFactory) {
   if (radioFactory) {
     delete radioFactory;
     delete radioFactory;
   }
   }
+  if (mqttClient) {
+    delete mqttClient;
+  }
 
 
   radioFactory = MiLightRadioFactory::fromSettings(settings);
   radioFactory = MiLightRadioFactory::fromSettings(settings);
 
 
@@ -76,6 +82,11 @@ void applySettings() {
   milightClient = new MiLightClient(radioFactory);
   milightClient = new MiLightClient(radioFactory);
   milightClient->begin();
   milightClient->begin();
 
 
+  if (settings.mqttServer().length() > 0) {
+    mqttClient = new MqttClient(settings, milightClient);
+    mqttClient->begin();
+  }
+
   initMilightUdpServers();
   initMilightUdpServers();
 }
 }
 
 
@@ -117,6 +128,10 @@ void setup() {
 void loop() {
 void loop() {
   httpServer->handleClient();
   httpServer->handleClient();
 
 
+  if (mqttClient) {
+    mqttClient->handleClient();
+  }
+
   if (udpServers) {
   if (udpServers) {
     for (size_t i = 0; i < settings.numGatewayConfigs; i++) {
     for (size_t i = 0; i < settings.numGatewayConfigs; i++) {
       udpServers[i]->handleClient();
       udpServers[i]->handleClient();