浏览代码

first stab at MQTT subscriber

Chris Mullins 8 年之前
父节点
当前提交
3fd016ec43

+ 2 - 1
data/web/index.html

@@ -124,7 +124,8 @@
   <script lang="text/javascript">
     var FORM_SETTINGS = [
       "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", "radio_interface_type", "mqtt_server",
+      "mqtt_topic_pattern", "mqtt_username", "mqtt_password"
     ];
 
     var FORM_SETTINGS_HELP = {

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


+ 2 - 1
lib/WebServer/TokenIterator.h

@@ -1,7 +1,8 @@
 #include <Arduino.h>
 
 #ifndef _TOKEN_ITERATOR_H
-#define _TOKEN_ITERATOR_H value
+#define _TOKEN_ITERATOR_H
+
 class TokenIterator {
 public:
   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 (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;
+}

+ 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

+ 108 - 0
lib/MQTT/MqttClient.cpp

@@ -0,0 +1,108 @@
+#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();
+}
+
+void MqttClient::reconnect() {
+  if (! mqttClient->connected()) {
+#ifdef MQTT_DEBUG
+    Serial.println("MqttClient - econnecting");
+#endif
+    char nameBuffer[30];
+    sprintf_P(nameBuffer, PSTR("milight-hub-%u"), ESP.getChipId());
+
+    if (mqttClient->connect(nameBuffer)) {
+      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;
+  const char* strPayload = reinterpret_cast<const char*>(payload);
+
+#ifdef MQTT_DEBUG
+  printf_P(PSTR("MqttClient - Got message on topic: %s\n%s\n"), topic, strPayload);
+#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(strPayload);
+
+  milightClient->prepare(*config, deviceId, groupId);
+  milightClient->update(obj);
+}

+ 29 - 0
lib/MQTT/MqttClient.h

@@ -0,0 +1,29 @@
+#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;
+
+  void subscribe();
+  void publishCallback(char* topic, byte* payload, int length);
+};
+
+#endif

+ 78 - 0
lib/MiLight/MiLightClient.cpp

@@ -217,6 +217,84 @@ void MiLightClient::command(uint8_t command, uint8_t arg) {
   flushPacket();
 }
 
+void MiLightClient::update(const JsonObject& request) {
+  if (request.containsKey("status")) {
+    const String& statusStr = request.get<String>("status");
+    MiLightStatus status = (statusStr == "on" || statusStr == "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("level")) {
+    this->updateBrightness(request["level"]);
+  }
+
+  if (request.containsKey("temperature")) {
+    this->updateTemperature(request["temperature"]);
+  }
+
+  if (request.containsKey("saturation")) {
+    this->updateSaturation(request["saturation"]);
+  }
+
+  if (request.containsKey("mode")) {
+    this->updateMode(request["mode"]);
+  }
+}
+
 void MiLightClient::formatPacket(uint8_t* packet, char* buffer) {
   formatter->format(packet, buffer);
 }

+ 2 - 0
lib/MiLight/MiLightClient.h

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

+ 42 - 72
lib/Settings/Settings.cpp

@@ -4,6 +4,8 @@
 #include <IntParsing.h>
 #include <algorithm>
 
+#define PORT_POSITION(s) ( s.indexOf(':') )
+
 bool Settings::hasAuthSettings() {
   return adminUsername.length() > 0 && adminPassword.length() > 0;
 }
@@ -23,53 +25,7 @@ size_t Settings::getAutoRestartPeriod() {
 void Settings::deserialize(Settings& settings, String json) {
   DynamicJsonBuffer jsonBuffer;
   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) {
@@ -108,33 +64,23 @@ void Settings::updateGatewayConfigs(JsonArray& arr) {
 
 void Settings::patch(JsonObject& parsedSettings) {
   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")) {
-      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")) {
       JsonArray& arr = parsedSettings["device_ids"];
       updateDeviceIds(arr);
@@ -189,6 +135,10 @@ void Settings::serialize(Stream& stream, const bool prettyPrint) {
   root["packet_repeats"] = this->packetRepeats;
   root["http_repeat_factor"] = this->httpRepeatFactor;
   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) {
     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) {
   if (s.equalsIgnoreCase("lt8900")) {
     return LT8900;

+ 14 - 1
lib/Settings/Settings.h

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

+ 1 - 77
lib/WebServer/MiLightHttpServer.cpp

@@ -373,83 +373,7 @@ void MiLightHttpServer::handleUpdateGroup(const UrlTokenBindings* urlBindings) {
 }
 
 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) {

+ 0 - 34
lib/WebServer/PatternHandler.cpp

@@ -1,39 +1,5 @@
 #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(
     const String& pattern,
     const HTTPMethod method,

+ 1 - 12
lib/WebServer/PatternHandler.h

@@ -5,18 +5,7 @@
 #include <ESP8266WebServer.h>
 #include <functional>
 #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 {
 public:

+ 1 - 0
platformio.ini

@@ -16,6 +16,7 @@ lib_deps_external =
   RF24
   WiFiManager
   ArduinoJson
+  PubSubClient
 build_flags = !python .get_version.py
 # -D MILIGHT_UDP_DEBUG
 # -D DEBUG_PRINTF

+ 14 - 0
src/main.cpp

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