Skip to content

Commit

Permalink
Merge branch 'dev'
Browse files Browse the repository at this point in the history
  • Loading branch information
bombcheck committed Aug 11, 2018
2 parents 8137ba6 + 4cd87ed commit 1379ada
Show file tree
Hide file tree
Showing 28 changed files with 736 additions and 69 deletions.
2 changes: 1 addition & 1 deletion .prepare_release
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ prepare_log() {
echo "[prepare release] -- $@"
}

if [ -z "$(git tag -l --points-at HEAD)" ]; then
if ! git describe --exact-match HEAD 2>/dev/null; then
prepare_log "Skipping non-tagged commit."
exit 0
fi
Expand Down
21 changes: 21 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License

Copyright (c) 2018 Chris Mullins

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
38 changes: 29 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@ Fork of [sidoh's great work](https://github.com/sidoh/esp8266_milight_hub) to fi

This is a replacement for a Milight/LimitlessLED remote/gateway hosted on an ESP8266. Leverages [Henryk Plötz's awesome reverse-engineering work](https://hackaday.io/project/5888-reverse-engineering-the-milight-on-air-protocol).

[Milight bulbs](https://www.amazon.com/Mi-light-Dimmable-RGBWW-Spotlight-Smart/dp/B01LPRQ4BK/r) are cheap smart bulbs that are controllable with an undocumented 2.4 GHz protocol. In order to control them, you either need a [remote](https://www.amazon.com/Mi-light-Dimmable-RGBWW-Spotlight-Smart/dp/B01LCSALV6/r?th=1) ($13), which allows you to control them directly, or a [WiFi gateway](https://www.amazon.com/BTF-LIGHTING-Mi-Light-WiFi-Bridge-Controller/dp/B01H87DYR8/ref=sr_1_7?ie=UTF8&qid=1485715984&sr=8-7&keywords=milight) ($30), which allows you to control them with a mobile app or a [UDP protocol](http://www.limitlessled.com/dev/).
[Milight bulbs](https://www.amazon.com/Mi-light-Dimmable-RGBWW-Spotlight-Smart/dp/B01LPRQ4BK/r) are cheap smart bulbs that are controllable with an undocumented 2.4 GHz protocol. In order to control them, you either need a [remote](https://www.amazon.com/Mi-light-Dimmable-RGBWW-Spotlight-Smart/dp/B01LCSALV6/r?th=1) ($13), which allows you to control them directly, or a [WiFi gateway](http://futlight.com/productlist.aspx?typeid=125) ($30), which allows you to control them with a mobile app or a [UDP protocol](https://github.com/Fantasmos/LimitlessLED-DevAPI).

This project is a replacement for the wifi gateway.

[This guide](http://blog.christophermullins.com/2017/02/11/milight-wifi-gateway-emulator-on-an-esp8266/) on my blog details setting one of these up.

Expand All @@ -15,16 +17,22 @@ This is a replacement for a Milight/LimitlessLED remote/gateway hosted on an ESP
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
## Supported remotes

The following remotes can be emulated:

Support has been added for the following [bulb types](http://futlight.com/productlist.aspx?typeid=101):

1. RGBW bulbs: FUT014, FUT016, FUT103
1. Dual-White (CCT) bulbs: FUT019
1. RGB LED strips: FUT025
1. RGB + Dual White (RGB+CCT) bulbs: FUT015, FUT105
Model #|Name|Compatible Bulbs
-------|-----------|----------------
|FUT096|RGB/W|<ol><li>FUT014</li><li>FUT016</li><li>FUT103</li>|
|FUT005, FUT006,FUT007</li></ol>|CCT|<ol><li>FUT011</li><li>FUT017</li><li>FUT019</li></ol>|
|FUT098|RGB|Most RGB LED Strip Controlers|
|FUT092|RGB/CCT|<ol><li>FUT012</li><li>FUT013</li><li>FUT014</li><li>FUT015</li><li>FUT103</li><li>FUT104</li><li>FUT105</li><li>Many RGB/CCT LED Strip Controllers</li></ol>|
|FUT091|CCT v2|Most newer dual white bulbs and controllers|
|FUT089|8-zone RGB/CCT|Most newer rgb + dual white bulbs and controllers|

Other bulb types might work, but have not been tested. It is also relatively easy to add support for new bulb types.
Other remotes or bulbs, but have not been tested.

## What you'll need

Expand Down Expand Up @@ -159,14 +167,14 @@ If you'd like to control bulbs in all groups paired with a particular device ID,
Turn on group 2 for device ID 0xCD86, set hue to 100, and brightness to 50%:
```
$ curl --data-binary '{"status":"on","hue":100,"level":50}' -X PUT http://esp8266/gateways/0xCD86/rgbw/2
$ curl -X PUT -H 'Content-Type: applicaiton/json' -d '{"status":"on","hue":100,"level":50}' http://esp8266/gateways/0xCD86/rgbw/2
true%
```
Set color to white (disable RGB):
```
$ curl --data-binary '{"command":"set_white"}' -X PUT http://esp8266/gateways/0xCD86/rgbw/2
$ curl -X PUT -H 'Content-Type: applicaiton/json' -d '{"command":"set_white"}' -X PUT http://esp8266/gateways/0xCD86/rgbw/2
true%
```
Expand Down Expand Up @@ -263,3 +271,15 @@ You can select between versions 5 and 6 of the UDP protocol (documented [here](h

[info-license]: https://github.com/sidoh/esp8266_milight_hub/blob/master/LICENSE
[shield-license]: https://img.shields.io/badge/license-MIT-blue.svg

## Donating

If the project brings you happiness or utility, it's more than enough for me to hear those words.

If you're feeling especially generous, and are open to a charitable donation, that'd make me very happy. Here are some whose mission I support (in no particular order):

* [Water.org](https://www.water.org)
* [Brain & Behavior Research Foundation](https://www.bbrfoundation.org/)
* [Electronic Frontier Foundation](https://www.eff.org/)
* [Girls Who Code](https://girlswhocode.com/)
* [San Francisco Animal Care & Control](http://www.sfanimalcare.org/make-a-donation/)
4 changes: 2 additions & 2 deletions dist/index.html.gz.h

Large diffs are not rendered by default.

6 changes: 6 additions & 0 deletions lib/MiLight/FUT089PacketFormatter.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,12 @@ void FUT089PacketFormatter::enableNightMode() {
}

BulbId FUT089PacketFormatter::parsePacket(const uint8_t *packet, JsonObject& result) {
if (stateStore == NULL) {
Serial.println(F("ERROR: stateStore not set. Prepare was not called! **THIS IS A BUG**"));
BulbId fakeId(0, 0, REMOTE_TYPE_FUT089);
return fakeId;
}

uint8_t packetCopy[V2_PACKET_LEN];
memcpy(packetCopy, packet, V2_PACKET_LEN);
V2RFEncoding::decodeV2Packet(packetCopy);
Expand Down
57 changes: 57 additions & 0 deletions lib/MiLight/FUT091PacketFormatter.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
#include <FUT091PacketFormatter.h>
#include <V2RFEncoding.h>
#include <Units.h>

static const uint8_t BRIGHTNESS_SCALE_MAX = 0x97;
static const uint8_t KELVIN_SCALE_MAX = 0xC5;

void FUT091PacketFormatter::updateBrightness(uint8_t value) {
command(static_cast<uint8_t>(FUT091Command::BRIGHTNESS), V2PacketFormatter::tov2scale(value, BRIGHTNESS_SCALE_MAX, 2));
}

void FUT091PacketFormatter::updateTemperature(uint8_t value) {
command(static_cast<uint8_t>(FUT091Command::KELVIN), V2PacketFormatter::tov2scale(value, KELVIN_SCALE_MAX, 2, false));
}

void FUT091PacketFormatter::enableNightMode() {
uint8_t arg = groupCommandArg(OFF, groupId);
command(static_cast<uint8_t>(FUT091Command::ON_OFF) | 0x80, arg);
}

BulbId FUT091PacketFormatter::parsePacket(const uint8_t *packet, JsonObject& result) {
uint8_t packetCopy[V2_PACKET_LEN];
memcpy(packetCopy, packet, V2_PACKET_LEN);
V2RFEncoding::decodeV2Packet(packetCopy);

BulbId bulbId(
(packetCopy[2] << 8) | packetCopy[3],
packetCopy[7],
REMOTE_TYPE_FUT091
);

uint8_t command = (packetCopy[V2_COMMAND_INDEX] & 0x7F);
uint8_t arg = packetCopy[V2_ARGUMENT_INDEX];

if (command == (uint8_t)FUT091Command::ON_OFF) {
if ((packetCopy[V2_COMMAND_INDEX] & 0x80) == 0x80) {
result["command"] = "night_mode";
} else if (arg < 5) { // Group is not reliably encoded in group byte. Extract from arg byte
result["state"] = "ON";
bulbId.groupId = arg;
} else {
result["state"] = "OFF";
bulbId.groupId = arg-5;
}
} else if (command == (uint8_t)FUT091Command::BRIGHTNESS) {
uint8_t level = V2PacketFormatter::fromv2scale(arg, BRIGHTNESS_SCALE_MAX, 2, true);
result["brightness"] = Units::rescale<uint8_t, uint8_t>(level, 255, 100);
} else if (command == (uint8_t)FUT091Command::KELVIN) {
uint8_t kelvin = V2PacketFormatter::fromv2scale(arg, KELVIN_SCALE_MAX, 2, false);
result["color_temp"] = Units::whiteValToMireds(kelvin, 100);
} else {
result["button_id"] = command;
result["argument"] = arg;
}

return bulbId;
}
25 changes: 25 additions & 0 deletions lib/MiLight/FUT091PacketFormatter.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
#include <V2PacketFormatter.h>

#ifndef _FUT091_PACKET_FORMATTER_H
#define _FUT091_PACKET_FORMATTER_H

enum class FUT091Command {
ON_OFF = 0x01,
BRIGHTNESS = 0x2,
KELVIN = 0x03
};

class FUT091PacketFormatter : public V2PacketFormatter {
public:
FUT091PacketFormatter()
: V2PacketFormatter(0x21, 4) // protocol is 0x21, and there are 4 groups
{ }

virtual void updateBrightness(uint8_t value);
virtual void updateTemperature(uint8_t value);
virtual void enableNightMode();

virtual BulbId parsePacket(const uint8_t* packet, JsonObject& result);
};

#endif
12 changes: 10 additions & 2 deletions lib/MiLight/MiLightClient.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,14 @@ void MiLightClient::begin() {
}

switchRadio(static_cast<size_t>(0));

// Little gross to do this here as it's relying on global state. A better alternative
// would be to statically construct remote config factories which take in a stateStore
// and settings pointer. The objects could then be initialized by calling the factory
// in main.
for (size_t i = 0; i < MiLightRemoteConfig::NUM_REMOTES; i++) {
MiLightRemoteConfig::ALL_REMOTES[i]->packetFormatter->initialize(stateStore, settings);
}
}

void MiLightClient::setHeld(bool held) {
Expand Down Expand Up @@ -78,15 +86,15 @@ void MiLightClient::prepare(const MiLightRemoteConfig* config,
this->currentRemote = config;

if (deviceId >= 0 && groupId >= 0) {
currentRemote->packetFormatter->prepare(deviceId, groupId, stateStore, settings);
currentRemote->packetFormatter->prepare(deviceId, groupId);
}
}

void MiLightClient::prepare(const MiLightRemoteType type,
const uint16_t deviceId,
const uint8_t groupId
) {
prepare(MiLightRemoteConfig::fromType(type));
prepare(MiLightRemoteConfig::fromType(type), deviceId, groupId);
}

void MiLightClient::setResendCount(const unsigned int resendCount) {
Expand Down
23 changes: 18 additions & 5 deletions lib/MiLight/MiLightRemoteConfig.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,20 @@
*/
const MiLightRemoteConfig* MiLightRemoteConfig::ALL_REMOTES[] = {
&FUT096Config, // rgbw
&FUT091Config, // cct
&FUT007Config, // cct
&FUT092Config, // rgb+cct
&FUT098Config, // rgb
&FUT089Config // 8-group rgb+cct (b8, fut089)
&FUT089Config, // 8-group rgb+cct (b8, fut089)
&FUT091Config
};

const MiLightRemoteConfig* MiLightRemoteConfig::fromType(const String& type) {
if (type.equalsIgnoreCase("rgbw") || type.equalsIgnoreCase("fut096")) {
return &FUT096Config;
}

if (type.equalsIgnoreCase("cct") || type.equalsIgnoreCase("fut091")) {
return &FUT091Config;
if (type.equalsIgnoreCase("cct") || type.equalsIgnoreCase("fut007")) {
return &FUT007Config;
}

if (type.equalsIgnoreCase("rgb_cct") || type.equalsIgnoreCase("fut092")) {
Expand All @@ -32,6 +33,10 @@ const MiLightRemoteConfig* MiLightRemoteConfig::fromType(const String& type) {
return &FUT098Config;
}

if (type.equalsIgnoreCase("v2_cct") || type.equalsIgnoreCase("fut091")) {
return &FUT091Config;
}

Serial.print(F("MiLightRemoteConfig::fromType: ERROR - tried to fetch remote config for type: "));
Serial.println(type);

Expand Down Expand Up @@ -77,14 +82,22 @@ const MiLightRemoteConfig FUT096Config( //rgbw
4
);

const MiLightRemoteConfig FUT091Config( //cct
const MiLightRemoteConfig FUT007Config( //cct
new CctPacketFormatter(),
MiLightRadioConfig::ALL_CONFIGS[1],
REMOTE_TYPE_CCT,
"cct",
4
);

const MiLightRemoteConfig FUT091Config( //v2 cct
new FUT091PacketFormatter(),
MiLightRadioConfig::ALL_CONFIGS[2],
REMOTE_TYPE_FUT091,
"fut091",
4
);

const MiLightRemoteConfig FUT092Config( //rgb+cct
new RgbCctPacketFormatter(),
MiLightRadioConfig::ALL_CONFIGS[2],
Expand Down
6 changes: 4 additions & 2 deletions lib/MiLight/MiLightRemoteConfig.h
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
#include <RgbCctPacketFormatter.h>
#include <CctPacketFormatter.h>
#include <FUT089PacketFormatter.h>
#include <FUT091PacketFormatter.h>
#include <PacketFormatter.h>

#ifndef _MILIGHT_REMOTE_CONFIG_H
Expand Down Expand Up @@ -36,14 +37,15 @@ class MiLightRemoteConfig {
static const MiLightRemoteConfig* fromType(const String& type);
static const MiLightRemoteConfig* fromReceivedPacket(const MiLightRadioConfig& radioConfig, const uint8_t* packet, const size_t len);

static const size_t NUM_REMOTES = 5;
static const size_t NUM_REMOTES = 6;
static const MiLightRemoteConfig* ALL_REMOTES[NUM_REMOTES];
};

extern const MiLightRemoteConfig FUT096Config; //rgbw
extern const MiLightRemoteConfig FUT091Config; //cct
extern const MiLightRemoteConfig FUT007Config; //cct
extern const MiLightRemoteConfig FUT092Config; //rgb+cct
extern const MiLightRemoteConfig FUT089Config; //rgb+cct B8 / FUT089
extern const MiLightRemoteConfig FUT098Config; //rgb
extern const MiLightRemoteConfig FUT091Config; //v2 cct

#endif
9 changes: 6 additions & 3 deletions lib/MiLight/PacketFormatter.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,11 @@ PacketFormatter::PacketFormatter(const size_t packetLength, const size_t maxPack
packetStream.packetLength = packetLength;
}

void PacketFormatter::initialize(GroupStateStore* stateStore, const Settings* settings) {
this->stateStore = stateStore;
this->settings = settings;
}

bool PacketFormatter::canHandle(const uint8_t *packet, const size_t len) {
return len == packetLength;
}
Expand Down Expand Up @@ -116,11 +121,9 @@ void PacketFormatter::valueByStepFunction(StepFunction increase, StepFunction de
}
}

void PacketFormatter::prepare(uint16_t deviceId, uint8_t groupId, GroupStateStore* stateStore, const Settings* settings) {
void PacketFormatter::prepare(uint16_t deviceId, uint8_t groupId) {
this->deviceId = deviceId;
this->groupId = groupId;
this->stateStore = stateStore;
this->settings = settings;
reset();
}

Expand Down
9 changes: 8 additions & 1 deletion lib/MiLight/PacketFormatter.h
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,13 @@ class PacketFormatter {
public:
PacketFormatter(const size_t packetLength, const size_t maxPackets = 1);

// Ideally these would be constructor parameters. We could accomplish this by
// wrapping PacketFormaters in a factory, as Settings and StateStore are not
// available at construction time.
//
// For now, just rely on the user calling this method.
void initialize(GroupStateStore* stateStore, const Settings* settings);

typedef void (PacketFormatter::*StepFunction)();

virtual bool canHandle(const uint8_t* packet, const size_t len);
Expand Down Expand Up @@ -72,7 +79,7 @@ class PacketFormatter {
virtual void reset();

virtual PacketStream& buildPackets();
virtual void prepare(uint16_t deviceId, uint8_t groupId, GroupStateStore* stateStore, const Settings* settings);
virtual void prepare(uint16_t deviceId, uint8_t groupId);
virtual void format(uint8_t const* packet, char* buffer);

virtual BulbId parsePacket(const uint8_t* packet, JsonObject& result);
Expand Down
Loading

0 comments on commit 1379ada

Please sign in to comment.