Skip to content

Commit a429163

Browse files
committed
Merge remote-tracking branch 'sidoh/v1.10.0-wip' into dev
2 parents 7a09c04 + 06e5c39 commit a429163

23 files changed

Lines changed: 349 additions & 80 deletions

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
.sconsign.dblite
77
/web/node_modules
88
/web/build
9+
/web/package-lock.json
910
/dist/*.bin
1011
/dist/docs
1112
.vscode/

.prepare_docs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,18 +31,21 @@ DIST_DIR="./dist/docs"
3131
BRANCHES_DIR="${DIST_DIR}/branches"
3232
API_SPEC_FILE="${DOCS_DIR}/openapi.yaml"
3333

34+
rm -rf "${DIST_DIR}"
35+
3436
redoc_bundle_file=$(mktemp)
3537
git_ref_version=$(git describe --always)
3638
branch_docs_dir="${BRANCHES_DIR}/${git_ref_version}"
3739

3840
# Build Redoc bundle (a single HTML file)
39-
redoc-cli bundle ${API_SPEC_FILE} -o ${redoc_bundle_file}
41+
redoc-cli bundle ${API_SPEC_FILE} -o ${redoc_bundle_file} --title 'Milight Hub API Documentation'
4042

4143
# Check out current stuff from gh-pages (we'll append to it)
4244
git fetch origin 'refs/heads/gh-pages:refs/heads/gh-pages'
4345
git checkout gh-pages -- branches || prepare_docs_log "Failed to checkout branches from gh-pages, skipping..."
4446

4547
if [ -e "./branches" ]; then
48+
mkdir -p "${DIST_DIR}"
4649
mv "./branches" "${BRANCHES_DIR}"
4750
else
4851
mkdir -p "${BRANCHES_DIR}"
@@ -62,7 +65,7 @@ cp "${redoc_bundle_file}" "${branch_docs_dir}/index.html"
6265

6366
# Update `latest` symlink to this branch
6467
rm -rf "${BRANCHES_DIR}/latest"
65-
ln -s "${branch_docs_dir}" "${BRANCHES_DIR}/latest"
68+
ln -s "${git_ref_version}" "${BRANCHES_DIR}/latest"
6669

6770
# Create a JSON file containing a list of all branches with docs (we'll
6871
# have an index page that renders the list).

README.md

Lines changed: 56 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -116,26 +116,6 @@ If it does not work as expected see [Troubleshooting](https://github.com/sidoh/e
116116

117117
If you need to pair some bulbs, how to do this is [described in the wiki](https://github.com/sidoh/esp8266_milight_hub/wiki/Pairing-new-bulbs).
118118

119-
## LED Status
120-
121-
Some ESP boards have a built-in LED, on pin #2. This LED will flash to indicate the current status of the hub:
122-
123-
* Wifi not configured: Fast flash (on/off once per second). See [Configure Wifi](#configure-wifi) to configure the hub.
124-
* Wifi connected and ready: Occasional blips of light (a flicker of light every 1.5 seconds).
125-
* Packets sending/receiving: Rapid blips of light for brief periods (three rapid flashes).
126-
* Wifi failed to configure: Solid light.
127-
128-
In the setup UI, you can turn on "enable_solid_led" to change the LED behavior to:
129-
130-
* Wifi connected and ready: Solid LED light
131-
* Wifi failed to configure: Light off
132-
133-
Note that you must restart the hub to affect the change in "enable_solid_led".
134-
135-
You can configure the LED pin from the web console. Note that pin means the GPIO number, not the D number ... for example, D2 is actually GPIO4 and therefore its pin 4. If you specify the pin as a negative number, it will invert the LED signal (the built-in LED on pin 2 is inverted, so the default is -2).
136-
137-
If you want to wire up your own LED on a pin, such as on D2/GPIO4, put a wire from D2 to one side of a 220 ohm resister. On the other side, connect it to the positive side (the longer wire) of a 3.3V LED. Then connect the negative side of the LED (the shorter wire) to ground. If you use a different voltage LED, or a high current LED, you will need to add a driver circuit.
138-
139119
## Device Aliases
140120

141121
You can configure aliases or labels for a given _(Device Type, Device ID, Group ID)_ tuple. For example, you might want to call the RGB+CCT remote with the ID `0x1111` and the Group ID `1` to be called `living_room`. Aliases are useful in a couple of different ways:
@@ -150,7 +130,7 @@ The REST API is specified using
150130

151131
[openapi.yaml](openapi.yaml) contains the raw spec, created using [OpenAPI v3](https://swagger.io/docs/specification/about/).
152132

153-
[You can view generated documentation for the master branch here.](https://sidoh.github.io/esp8266_milight_hub/master)
133+
[You can view generated documentation for the master branch here.](https://sidoh.github.io/esp8266_milight_hub/branches/latest)
154134

155135
[Docs for other branches can be found here](https://sidoh.github.io/esp8266_milight_hub)
156136

@@ -244,6 +224,61 @@ You can add an arbitrary number of UDP gateways through the REST API or through
244224

245225
You can select between versions 5 and 6 of the UDP protocol (documented [here](http://www.limitlessled.com/dev/)). Version 6 has support for the newer RGB+CCT bulbs and also includes response packets, which can theoretically improve reliability. Version 5 has much smaller packets and is probably lower latency.
246226

227+
## Transitions
228+
229+
Transitions between two given states are supported. Depending on how transition commands are being issued, the duration and smoothness of the transition are both configurable. There are a few ways to use transitions:
230+
231+
#### RESTful `/transitions` routes
232+
233+
These routes are fully documented in the [REST API documentation](https://sidoh.github.io/esp8266_milight_hub/branches/latest/#tag/Transitions).
234+
235+
#### `transition` field when issuing commands
236+
237+
When you issue a command to a bulb either via REST or MQTT, you can include a `transition` field. The value of this field specifies the duration of the transition, in seconds (non-integer values are supported).
238+
239+
For example, the command:
240+
241+
```json
242+
{"brightness":255,"transition":60}
243+
```
244+
245+
will transition from whatever the current brightness is to `brightness=255` over 60 seconds.
246+
247+
#### Notes on transitions
248+
249+
* espMH's transitions should work seamlessly with [HomeAssistant's transition functionality](https://www.home-assistant.io/components/light/).
250+
* You can issue commands specifying transitions between many fields at once. For example:
251+
```json
252+
{"brightness":255,"kelvin":0,"transition":10.5}
253+
```
254+
will transition from current values for brightness and kelvin to the specified values -- 255 and 0 respectively -- over 10.5 seconds.
255+
* Color transitions are supported. Under the hood, this is treated as a transition between current values for r, g, and b to the r, g, b values for the specified color. Because milight uses hue-sat colors, this might not behave exactly as you'd expect for all colors.
256+
* You can transition to a given `status` or `state`. For example,
257+
```json
258+
{"status":"ON","transition":10}
259+
```
260+
will turn the bulb on, immediately set the brightness to 0, and then transition to brightness=255 over 10 seconds. If you specify a brightness value, the transition will stop there instead of 255.
261+
262+
## LED Status
263+
264+
Some ESP boards have a built-in LED, on pin #2. This LED will flash to indicate the current status of the hub:
265+
266+
* Wifi not configured: Fast flash (on/off once per second). See [Configure Wifi](#configure-wifi) to configure the hub.
267+
* Wifi connected and ready: Occasional blips of light (a flicker of light every 1.5 seconds).
268+
* Packets sending/receiving: Rapid blips of light for brief periods (three rapid flashes).
269+
* Wifi failed to configure: Solid light.
270+
271+
In the setup UI, you can turn on "enable_solid_led" to change the LED behavior to:
272+
273+
* Wifi connected and ready: Solid LED light
274+
* Wifi failed to configure: Light off
275+
276+
Note that you must restart the hub to affect the change in "enable_solid_led".
277+
278+
You can configure the LED pin from the web console. Note that pin means the GPIO number, not the D number ... for example, D2 is actually GPIO4 and therefore its pin 4. If you specify the pin as a negative number, it will invert the LED signal (the built-in LED on pin 2 is inverted, so the default is -2).
279+
280+
If you want to wire up your own LED on a pin, such as on D2/GPIO4, put a wire from D2 to one side of a 220 ohm resister. On the other side, connect it to the positive side (the longer wire) of a 3.3V LED. Then connect the negative side of the LED (the shorter wire) to ground. If you use a different voltage LED, or a high current LED, you will need to add a driver circuit.
281+
247282
## Development
248283

249284
This project is developed and built using [PlatformIO](https://platformio.org/).

dist/index.html.gz.h

Lines changed: 2 additions & 2 deletions
Large diffs are not rendered by default.

docs/openapi.yaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -771,6 +771,7 @@ components:
771771
- group_id
772772
- device_type
773773
- oh_color
774+
- hex_color
774775
description: >
775776
Defines a field which is a part of state for a particular light device. Most fields are self-explanatory, but documentation for each follows:
776777
@@ -786,6 +787,8 @@ components:
786787
787788
* `oh_color` - same as `color` with a format compatible with [OpenHAB's colorRGB channel type](https://www.openhab.org/addons/bindings/mqtt.generic/#channel-type-colorrgb-colorhsb).
788789
790+
* `hex_color` - same as `color` except in hex color (e.g., `#FF0000` for red).
791+
789792
* `device_id` / `device_type` / `group_id` - this information is in the MQTT topic or REST route, but can be included in the payload in the case that processing the topic or route is more difficult.
790793
DeviceId:
791794
type: array

lib/MQTT/HomeAssistantDiscoveryClient.cpp

Lines changed: 35 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,14 @@ void HomeAssistantDiscoveryClient::addConfig(const char* alias, const BulbId& bu
4040
config[F("name")] = alias;
4141
config[F("command_topic")] = mqttClient->bindTopicString(settings.mqttTopicPattern, bulbId);
4242
config[F("state_topic")] = mqttClient->bindTopicString(settings.mqttStateTopicPattern, bulbId);
43+
JsonObject deviceMetadata = config.createNestedObject(F("device"));
44+
45+
deviceMetadata[F("manufacturer")] = F("esp8266_milight_hub");
46+
deviceMetadata[F("sw_version")] = QUOTE(MILIGHT_HUB_VERSION);
47+
48+
JsonArray identifiers = deviceMetadata.createNestedArray(F("identifiers"));
49+
identifiers.add(ESP.getChipId());
50+
bulbId.serialize(identifiers);
4351

4452
// HomeAssistant only supports simple client availability
4553
if (settings.mqttClientStatusTopic.length() > 0 && settings.simpleMqttClientStatus) {
@@ -57,36 +65,48 @@ void HomeAssistantDiscoveryClient::addConfig(const char* alias, const BulbId& bu
5765
JsonArray effects = config.createNestedArray(F("effect_list"));
5866
effects.add(MiLightCommandNames::NIGHT_MODE);
5967

60-
// These bulbs support RGB color
68+
// These bulbs support switching between rgb/white, and have a "white_mode" command
6169
switch (bulbId.deviceType) {
6270
case REMOTE_TYPE_FUT089:
63-
case REMOTE_TYPE_RGB:
6471
case REMOTE_TYPE_RGB_CCT:
6572
case REMOTE_TYPE_RGBW:
66-
config[F("rgb")] = true;
73+
effects.add("white_mode");
6774
break;
6875
default:
6976
break; //nothing
7077
}
7178

72-
// These bulbs support adjustable white values
79+
// All bulbs except CCT have 9 modes. FUT029 and RGB/FUT096 has 9 modes, but they
80+
// are not selectable directly. There are only "next mode" commands.
7381
switch (bulbId.deviceType) {
7482
case REMOTE_TYPE_CCT:
83+
case REMOTE_TYPE_RGB:
84+
case REMOTE_TYPE_FUT020:
85+
break;
86+
default:
87+
addNumberedEffects(effects, 0, 8);
88+
break;
89+
}
90+
91+
// These bulbs support RGB color
92+
switch (bulbId.deviceType) {
7593
case REMOTE_TYPE_FUT089:
76-
case REMOTE_TYPE_FUT091:
94+
case REMOTE_TYPE_RGB:
7795
case REMOTE_TYPE_RGB_CCT:
78-
config[GroupStateFieldNames::COLOR_TEMP] = true;
96+
case REMOTE_TYPE_RGBW:
97+
config[F("rgb")] = true;
7998
break;
8099
default:
81100
break; //nothing
82101
}
83102

84-
// These bulbs support switching between rgb/white, and have a "white_mode" command
103+
// These bulbs support adjustable white values
85104
switch (bulbId.deviceType) {
105+
case REMOTE_TYPE_CCT:
86106
case REMOTE_TYPE_FUT089:
107+
case REMOTE_TYPE_FUT091:
87108
case REMOTE_TYPE_RGB_CCT:
88-
case REMOTE_TYPE_RGBW:
89-
effects.add("white_mode");
109+
config[GroupStateFieldNames::COLOR_TEMP] = true;
90110
break;
91111
default:
92112
break; //nothing
@@ -144,4 +164,10 @@ String HomeAssistantDiscoveryClient::bindTopicVariables(const String& topic, con
144164
boundTopic.replace(":group_id", String(bulbId.groupId));
145165

146166
return boundTopic;
167+
}
168+
169+
void HomeAssistantDiscoveryClient::addNumberedEffects(JsonArray& effectList, uint8_t start, uint8_t end) {
170+
for (uint8_t i = start; i <= end; ++i) {
171+
effectList.add(String(i));
172+
}
147173
}

lib/MQTT/HomeAssistantDiscoveryClient.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,4 +20,5 @@ class HomeAssistantDiscoveryClient {
2020

2121
String buildTopic(const BulbId& bulbId);
2222
String bindTopicVariables(const String& topic, const char* alias, const BulbId& bulbId);
23+
void addNumberedEffects(JsonArray& effectList, uint8_t start, uint8_t end);
2324
};

lib/MiLight/MiLightClient.cpp

Lines changed: 43 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010

1111
using namespace std::placeholders;
1212

13+
static const uint8_t STATUS_UNDEFINED = 255;
14+
1315
const char* MiLightClient::FIELD_ORDERINGS[] = {
1416
// These are handled manually
1517
// GroupStateFieldNames::STATE,
@@ -20,6 +22,7 @@ const char* MiLightClient::FIELD_ORDERINGS[] = {
2022
GroupStateFieldNames::TEMPERATURE,
2123
GroupStateFieldNames::COLOR_TEMP,
2224
GroupStateFieldNames::MODE,
25+
GroupStateFieldNames::EFFECT,
2326
GroupStateFieldNames::COLOR,
2427
// Level/Brightness must be processed last because they're specific to a particular bulb mode.
2528
// So make sure bulb mode is set before applying level/brightness.
@@ -279,7 +282,7 @@ void MiLightClient::updateColor(JsonVariant json) {
279282
ParsedColor color = ParsedColor::fromJson(json);
280283

281284
if (!color.success) {
282-
Serial.println(F("Error parsing JSON color"));
285+
Serial.println(F("Error parsing color field, unrecognized format"));
283286
return;
284287
}
285288

@@ -320,7 +323,20 @@ void MiLightClient::update(JsonObject request) {
320323
if (transition == 0) {
321324
this->updateStatus(ON);
322325
} else {
323-
handleTransition(GroupStateField::STATUS, status, transition);
326+
JsonVariant brightness = request[GroupStateFieldNames::BRIGHTNESS];
327+
JsonVariant level = request[GroupStateFieldNames::LEVEL];
328+
329+
// The behavior for status transitions is to ramp up to max or down to min brightness. If a
330+
// brightness is specified, we shold ramp up or down to that value instead.
331+
if (!brightness.isUndefined()) {
332+
this->updateStatus(ON);
333+
handleTransition(GroupStateField::BRIGHTNESS, brightness, transition, 0);
334+
} else if (!level.isUndefined()) {
335+
this->updateStatus(ON);
336+
handleTransition(GroupStateField::LEVEL, level, transition, 0);
337+
} else {
338+
handleTransition(GroupStateField::STATUS, status, transition, 0);
339+
}
324340
}
325341
}
326342

@@ -330,14 +346,20 @@ void MiLightClient::update(JsonObject request) {
330346
JsonVariant value = request[fieldName];
331347

332348
if (handler != FIELD_SETTERS.end()) {
333-
if (transition != 0) {
334-
handleTransition(
335-
GroupStateFieldHelpers::getFieldByName(fieldName),
336-
value,
337-
transition
338-
);
339-
} else {
349+
// No transition -- set field directly
350+
if (transition == 0) {
340351
handler->second(this, value);
352+
} else {
353+
// Do not generate a brightness transition if a status field was specified. Status will
354+
// generate its own brightness transition, and generating another one will cause conflicts.
355+
GroupStateField field = GroupStateFieldHelpers::getFieldByName(fieldName);
356+
357+
if ( !GroupStateFieldHelpers::isBrightnessField(field) // If field isn't brightness
358+
|| parsedStatus == STATUS_UNDEFINED // or if there was not a status field
359+
// in the command
360+
) {
361+
handleTransition(field, value, transition);
362+
}
341363
}
342364
}
343365
}
@@ -414,7 +436,7 @@ void MiLightClient::handleCommand(JsonVariant command) {
414436
}
415437
}
416438

417-
void MiLightClient::handleTransition(GroupStateField field, JsonVariant value, float duration) {
439+
void MiLightClient::handleTransition(GroupStateField field, JsonVariant value, float duration, int16_t startValue) {
418440
BulbId bulbId = currentRemote->packetFormatter->currentBulbId();
419441
GroupState* currentState = stateStore->get(bulbId);
420442
std::shared_ptr<Transition::Builder> transitionBuilder = nullptr;
@@ -439,22 +461,28 @@ void MiLightClient::handleTransition(GroupStateField field, JsonVariant value, f
439461
endColor
440462
);
441463
} else if (field == GroupStateField::STATUS || field == GroupStateField::STATE) {
442-
uint8_t startLevel = 0;
464+
uint8_t startLevel;
443465
MiLightStatus status = parseMilightStatus(value);
444466

445-
if (currentState->isSetBrightness()) {
467+
if (startValue == FETCH_VALUE_FROM_STATE) {
446468
startLevel = currentState->getBrightness();
447469
} else if (status == ON) {
448470
startLevel = 0;
449471
} else {
450472
startLevel = 100;
451473
}
452474

453-
transitionBuilder = transitions.buildStatusTransition(bulbId, parseMilightStatus(value), startLevel);
475+
transitionBuilder = transitions.buildStatusTransition(bulbId, status, startLevel);
454476
} else {
455-
uint16_t currentValue = currentState->getParsedFieldValue(field);
477+
uint16_t currentValue;
456478
uint16_t endValue = value;
457479

480+
if (startValue == FETCH_VALUE_FROM_STATE) {
481+
currentValue = currentState->getParsedFieldValue(field);
482+
} else {
483+
currentValue = startValue;
484+
}
485+
458486
transitionBuilder = transitions.buildFieldTransition(
459487
bulbId,
460488
field,
@@ -598,7 +626,7 @@ JsonVariant MiLightClient::extractStatus(JsonObject object) {
598626

599627
uint8_t MiLightClient::parseStatus(JsonVariant val) {
600628
if (val.isUndefined()) {
601-
return 255;
629+
return STATUS_UNDEFINED;
602630
}
603631

604632
return parseMilightStatus(val);

lib/MiLight/MiLightClient.h

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,9 @@ namespace TransitionParams {
3737

3838
class MiLightClient {
3939
public:
40+
// Used to indicate that the start value for a transition should be fetched from current state
41+
static const int16_t FETCH_VALUE_FROM_STATE = -1;
42+
4043
MiLightClient(
4144
RadioSwitchboard& radioSwitchboard,
4245
PacketSender& packetSender,
@@ -93,7 +96,7 @@ class MiLightClient {
9396
void handleCommand(JsonVariant command);
9497
void handleCommands(JsonArray commands);
9598
bool handleTransition(JsonObject args, JsonDocument& responseObj);
96-
void handleTransition(GroupStateField field, JsonVariant value, float duration);
99+
void handleTransition(GroupStateField field, JsonVariant value, float duration, int16_t startValue = FETCH_VALUE_FROM_STATE);
97100
void handleEffect(const String& effect);
98101

99102
void onUpdateBegin(EventHandler handler);

0 commit comments

Comments
 (0)