Skip to content

Commit

Permalink
Merge pull request #89 from sidoh/passive_listen
Browse files Browse the repository at this point in the history
Add support for passive listens, publishing updates to MQTT
  • Loading branch information
sidoh authored Jul 2, 2017
2 parents 5e6ee84 + c0283e7 commit a70e2a3
Show file tree
Hide file tree
Showing 25 changed files with 355 additions and 115 deletions.
23 changes: 23 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ This is a replacement for a Milight/LimitlessLED remote/gateway hosted on an ESP
2. This project exposes a nice REST API to control your bulbs.
3. You can secure the ESP8266 with a username/password, which is more than you can say for the Milight gateway! (The 2.4 GHz protocol is still totally insecure, so this doesn't accomplish much :).
4. Official hubs connect to remote servers to enable WAN access, and this behavior is not disableable.
5. This project is capable of passively listening for Milight packets sent from other devices (like remotes). It can publish data from intercepted packets to MQTT. This could, for example, allow the use of Milight remotes while keeping your home automation platform's state in sync. See the MQTT section for more detail.

## Supported bulbs

Expand Down Expand Up @@ -174,6 +175,28 @@ irb(main):004:0> client.publish('milight/0x118D/rgb_cct/1', '{"status":"ON","col

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.

#### Updates

To enable passive listening, make sure that `listen_repeats` is set to something larger than 0 (the default value of 3 is a good choice).

To publish data from intercepted packets to an MQTT topic, configure MQTT server settings, and set the `mqtt_update_topic_pattern` to something of your choice. As with `mqtt_topic_pattern`, the tokens `:device_id`, `:device_type`, and `:group_id` will be substituted with the values from the relevant packet.

The published message is a JSON blob containing the following keys:

* `device_id`
* `device_type` (rgb_cct, rgbw, etc.)
* `group_id`
* Any number of: `status`, `level`, `hue`, `saturation`, `kelvin`

As an example, if `mqtt_update_topic_pattern` is set to `milight/updates/:device_id/:device_type/:group_id`, and the group 1 on button of a Milight remote is pressed, the following update will be dispatched:

```ruby
irb(main):005:0> client.subscribe('milight/updates/+/+/+')
=> 27
irb(main):006:0> puts client.get.inspect
["lights/updates/0x1C8E/rgb_cct/1", "{\"device_id\":7310,\"group_id\":1,\"device_type\":\"rgb_cct\",\"status\":\"on\"}"]
```

## 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.).
Expand Down
38 changes: 14 additions & 24 deletions data/web/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
.error-info:before { content: '('; }
.error-info:after { content: ')'; }
.header-btn { margin: 20px; }
#sniffed-traffic { max-height: 50em; overflow-y: auto; }
.btn-secondary {
background-color: #fff;
border: 1px solid #ccc;
Expand Down Expand Up @@ -124,8 +125,9 @@
<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", "discovery_port", "mqtt_server",
"mqtt_topic_pattern", "mqtt_username", "mqtt_password", "radio_interface_type"
"http_repeat_factor", "auto_restart_period", "discovery_port", "mqtt_server",
"mqtt_topic_pattern", "mqtt_update_topic_pattern", "mqtt_username", "mqtt_password",
"radio_interface_type", "listen_repeats"
];

var FORM_SETTINGS_HELP = {
Expand All @@ -141,9 +143,14 @@
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.",
"lights/:device_id/:device_type/:group_id. See README for further details.",
mqtt_update_topic_pattern : "Pattern to publish MQTT updates. Packets that " +
"are received from other devices, and packets that are sent from this device will " +
"result in updates being sent.",
discovery_port : "UDP port to listen for discovery packets on. Defaults to " +
"the same port used by MiLight devices, 48899. Use 0 to disable."
"the same port used by MiLight devices, 48899. Use 0 to disable.",
listen_repeats : "Increasing this increases the amount of time spent listening for " +
"packets. Set to 0 to disable listening. Default is 3."
}

var UDP_PROTOCOL_VERSIONS = [ 5, 6 ];
Expand Down Expand Up @@ -207,10 +214,8 @@
var sniffRequest;
var sniffing = false;
var getTraffic = function() {
var sniffType = $('#sniff-type input:checked').data('value');

sniffRequest = $.get('/gateway_traffic/' + sniffType, function(data) {
$('#sniffed-traffic').html(data + $('#sniffed-traffic').html());
sniffRequest = $.get('/gateway_traffic', function(data) {
$('#sniffed-traffic').prepend('<pre>' + data + '</pre>');
getTraffic();
});
};
Expand Down Expand Up @@ -1006,24 +1011,9 @@ <h1>Sniff Traffic</h1>
<div class="col-sm-12">
<button type="button" id="sniff" class="btn btn-primary">Start Sniffing</button>

<div class="btn-group" id="sniff-type" data-toggle="buttons">
<label class="btn btn-secondary active">
<input type="radio" name="options" autocomplete="off" data-value="rgbw" checked> RGBW
</label>
<label class="btn btn-secondary">
<input type="radio" name="options" autocomplete="off" data-value="cct"> CCT
</label>
<label class="btn btn-secondary">
<input type="radio" name="options" autocomplete="off" data-value="rgb_cct"> RGB+CCT
</label>
<label class="btn btn-secondary">
<input type="radio" name="options" autocomplete="off" data-value="rgb"> RGB
</label>
</div>

<div> &nbsp; </div>

<pre id="sniffed-traffic"></pre>
<div id="sniffed-traffic"></div>
</div>
</div>

Expand Down
21 changes: 21 additions & 0 deletions lib/MQTT/MqttClient.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,27 @@ void MqttClient::handleClient() {
mqttClient->loop();
}

void MqttClient::sendUpdate(MiLightRadioType type, uint16_t deviceId, uint16_t groupId, const char* update) {
String topic = settings.mqttUpdateTopicPattern;

if (topic.length() == 0) {
return;
}

String deviceIdStr = String(deviceId, 16);
deviceIdStr.toUpperCase();

topic.replace(":device_id", String("0x") + deviceIdStr);
topic.replace(":group_id", String(groupId));
topic.replace(":device_type", MiLightRadioConfig::fromType(type)->name);

#ifdef MQTT_DEBUG
printf_P(PSTR("MqttClient - publishing update to %s: %s\n"), topic.c_str(), update);
#endif

mqttClient->publish(topic.c_str(), update);
}

void MqttClient::subscribe() {
String topic = settings.mqttTopicPattern;

Expand Down
1 change: 1 addition & 0 deletions lib/MQTT/MqttClient.h
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ class MqttClient {
void begin();
void handleClient();
void reconnect();
void sendUpdate(MiLightRadioType type, uint16_t deviceId, uint16_t groupId, const char* update);

private:
WiFiClient tcpClient;
Expand Down
52 changes: 52 additions & 0 deletions lib/MiLight/CctPacketFormatter.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,58 @@ uint8_t CctPacketFormatter::getCctStatusButton(uint8_t groupId, MiLightStatus st
return button;
}

uint8_t CctPacketFormatter::cctCommandIdToGroup(uint8_t command) {
switch (command & 0xF) {
case CCT_GROUP_1_ON:
case CCT_GROUP_1_OFF:
return 1;
case CCT_GROUP_2_ON:
case CCT_GROUP_2_OFF:
return 2;
case CCT_GROUP_3_ON:
case CCT_GROUP_3_OFF:
return 3;
case CCT_GROUP_4_ON:
case CCT_GROUP_4_OFF:
return 4;
case CCT_ALL_ON:
case CCT_ALL_OFF:
return 0;
}

return 255;
}

MiLightStatus CctPacketFormatter::cctCommandToStatus(uint8_t command) {
switch (command & 0xF) {
case CCT_GROUP_1_ON:
case CCT_GROUP_2_ON:
case CCT_GROUP_3_ON:
case CCT_GROUP_4_ON:
case CCT_ALL_ON:
return ON;
case CCT_GROUP_1_OFF:
case CCT_GROUP_2_OFF:
case CCT_GROUP_3_OFF:
case CCT_GROUP_4_OFF:
case CCT_ALL_OFF:
return OFF;
}
}

void CctPacketFormatter::parsePacket(const uint8_t* packet, JsonObject& result) {
uint8_t command = packet[CCT_COMMAND_INDEX] & 0x7F;

result["device_id"] = (packet[1] << 8) | packet[2];
result["device_type"] = "cct";
result["group_id"] = packet[3];

uint8_t onOffGroupId = cctCommandIdToGroup(command);
if (onOffGroupId < 255) {
result["status"] = cctCommandToStatus(command) == ON ? "on" : "off";
}
}

void CctPacketFormatter::format(uint8_t const* packet, char* buffer) {
PacketFormatter::formatV1Packet(packet, buffer);
}
3 changes: 3 additions & 0 deletions lib/MiLight/CctPacketFormatter.h
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,11 @@ class CctPacketFormatter : public PacketFormatter {

virtual void format(uint8_t const* packet, char* buffer);
virtual void initializePacket(uint8_t* packet);
virtual void parsePacket(const uint8_t* packet, JsonObject& result);

static uint8_t getCctStatusButton(uint8_t groupId, MiLightStatus status);
static uint8_t cctCommandIdToGroup(uint8_t command);
static MiLightStatus cctCommandToStatus(uint8_t command);
};

#endif
20 changes: 18 additions & 2 deletions lib/MiLight/MiLightClient.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@
MiLightClient::MiLightClient(MiLightRadioFactory* radioFactory)
: resendCount(MILIGHT_DEFAULT_RESEND_COUNT),
currentRadio(NULL),
numRadios(MiLightRadioConfig::NUM_CONFIGS)
numRadios(MiLightRadioConfig::NUM_CONFIGS),
packetSentHandler(NULL)
{
radios = new MiLightRadio*[numRadios];

Expand Down Expand Up @@ -63,7 +64,14 @@ void MiLightClient::prepare(MiLightRadioConfig& config,
const uint16_t deviceId,
const uint8_t groupId) {

switchRadio(config.type);
prepare(config.type, deviceId, groupId);
}

void MiLightClient::prepare(MiLightRadioType type,
const uint16_t deviceId,
const uint8_t groupId) {

switchRadio(type);

if (deviceId >= 0 && groupId >= 0) {
formatter->prepare(deviceId, groupId);
Expand Down Expand Up @@ -110,6 +118,10 @@ void MiLightClient::write(uint8_t packet[]) {
currentRadio->write(packet, currentRadio->config().getPacketLength());
}

if (this->packetSentHandler) {
this->packetSentHandler(packet, currentRadio->config());
}

#ifdef DEBUG_PRINTF
int iElapsed = millis() - iStart;
Serial.print("Elapsed: ");
Expand Down Expand Up @@ -375,3 +387,7 @@ void MiLightClient::flushPacket() {
setResendCount(prevNumRepeats);
formatter->reset();
}

void MiLightClient::onPacketSent(PacketSentHandler handler) {
this->packetSentHandler = handler;
}
9 changes: 8 additions & 1 deletion lib/MiLight/MiLightClient.h
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
#include <functional>
#include <Arduino.h>
#include <MiLightRadio.h>
#include <MiLightRadioFactory.h>
Expand All @@ -11,6 +12,7 @@

#define MILIGHT_DEFAULT_RESEND_COUNT 10


class MiLightClient {
public:
MiLightClient(MiLightRadioFactory* radioFactory);
Expand All @@ -19,8 +21,11 @@ class MiLightClient {
delete[] radios;
}

typedef std::function<void(uint8_t* packet, const MiLightRadioConfig& config)> PacketSentHandler;

void begin();
void prepare(MiLightRadioConfig& config, const uint16_t deviceId = -1, const uint8_t groupId = -1);
void prepare(MiLightRadioType config, const uint16_t deviceId = -1, const uint8_t groupId = -1);

void setResendCount(const unsigned int resendCount);
bool available();
Expand Down Expand Up @@ -62,14 +67,16 @@ class MiLightClient {
void update(const JsonObject& object);
void handleCommand(const String& command);

void onPacketSent(PacketSentHandler handler);

protected:

MiLightRadio** radios;
MiLightRadio* currentRadio;
PacketFormatter* formatter;
const size_t numRadios;

unsigned int resendCount;
PacketSentHandler packetSentHandler;

MiLightRadio* switchRadio(const MiLightRadioType type);
uint8_t parseStatus(const JsonObject& object);
Expand Down
30 changes: 18 additions & 12 deletions lib/MiLight/MiLightRadioConfig.cpp
Original file line number Diff line number Diff line change
@@ -1,26 +1,32 @@
#include <MiLightRadioConfig.h>
const MiLightRadioConfig* MiLightRadioConfig::ALL_CONFIGS[] = {

MiLightRadioConfig* MiLightRadioConfig::ALL_CONFIGS[] = {
&MilightRgbwConfig,
&MilightCctConfig,
&MilightRgbCctConfig,
&MilightRgbConfig
};

MiLightRadioConfig* MiLightRadioConfig::fromString(const String& s) {
if (s.equalsIgnoreCase("rgbw")) {
return &MilightRgbwConfig;
} else if (s.equalsIgnoreCase("cct")) {
return &MilightCctConfig;
} else if (s.equalsIgnoreCase("rgb_cct")) {
return &MilightRgbCctConfig;
} else if (s.equalsIgnoreCase("rgb")) {
return &MilightRgbConfig;
for (size_t i = 0; i < MiLightRadioConfig::NUM_CONFIGS; i++) {
MiLightRadioConfig* config = MiLightRadioConfig::ALL_CONFIGS[i];
if (s.equalsIgnoreCase(config->name)) {
return config;
}
}
return NULL;
}

MiLightRadioConfig* MiLightRadioConfig::fromType(MiLightRadioType type) {
for (size_t i = 0; i < MiLightRadioConfig::NUM_CONFIGS; i++) {
MiLightRadioConfig* config = MiLightRadioConfig::ALL_CONFIGS[i];
if (config->type == type) {
return config;
}
}

return NULL;
}

size_t MiLightRadioConfig::getPacketLength() const {
return packetFormatter->getPacketLength();
}
}
Loading

0 comments on commit a70e2a3

Please sign in to comment.