esp32_ethernet_milight_hub/lib/MiLight/MiLightClient.cpp

670 lines
21 KiB
C++

#include <MiLightClient.h>
#include <MiLightRadioConfig.h>
#include <Arduino.h>
#include <RGBConverter.h>
#include <Units.h>
#include <TokenIterator.h>
#include <ParsedColor.h>
#include <MiLightCommands.h>
#include <functional>
using namespace std::placeholders;
static const uint8_t STATUS_UNDEFINED = 255;
const char* MiLightClient::FIELD_ORDERINGS[] = {
// These are handled manually
// GroupStateFieldNames::STATE,
// GroupStateFieldNames::STATUS,
GroupStateFieldNames::HUE,
GroupStateFieldNames::SATURATION,
GroupStateFieldNames::KELVIN,
GroupStateFieldNames::TEMPERATURE,
GroupStateFieldNames::COLOR_TEMP,
GroupStateFieldNames::MODE,
GroupStateFieldNames::EFFECT,
GroupStateFieldNames::COLOR,
// Level/Brightness must be processed last because they're specific to a particular bulb mode.
// So make sure bulb mode is set before applying level/brightness.
GroupStateFieldNames::LEVEL,
GroupStateFieldNames::BRIGHTNESS,
GroupStateFieldNames::COMMAND,
GroupStateFieldNames::COMMANDS
};
const std::map<const char*, std::function<void(MiLightClient*, JsonVariant)>, MiLightClient::cmp_str> MiLightClient::FIELD_SETTERS = {
{
GroupStateFieldNames::STATUS,
[](MiLightClient* client, JsonVariant val) {
client->updateStatus(parseMilightStatus(val));
}
},
{GroupStateFieldNames::LEVEL, &MiLightClient::updateBrightness},
{
GroupStateFieldNames::BRIGHTNESS,
[](MiLightClient* client, uint16_t arg) {
client->updateBrightness(Units::rescale<uint16_t, uint16_t>(arg, 100, 255));
}
},
{GroupStateFieldNames::HUE, &MiLightClient::updateHue},
{GroupStateFieldNames::SATURATION, &MiLightClient::updateSaturation},
{GroupStateFieldNames::KELVIN, &MiLightClient::updateTemperature},
{GroupStateFieldNames::TEMPERATURE, &MiLightClient::updateTemperature},
{
GroupStateFieldNames::COLOR_TEMP,
[](MiLightClient* client, uint16_t arg) {
client->updateTemperature(Units::miredsToWhiteVal(arg, 100));
}
},
{GroupStateFieldNames::MODE, &MiLightClient::updateMode},
{GroupStateFieldNames::COLOR, &MiLightClient::updateColor},
{GroupStateFieldNames::EFFECT, &MiLightClient::handleEffect},
{GroupStateFieldNames::COMMAND, &MiLightClient::handleCommand},
{GroupStateFieldNames::COMMANDS, &MiLightClient::handleCommands}
};
MiLightClient::MiLightClient(
RadioSwitchboard& radioSwitchboard,
PacketSender& packetSender,
GroupStateStore* stateStore,
Settings& settings,
TransitionController& transitions
) : radioSwitchboard(radioSwitchboard)
, updateBeginHandler(NULL)
, updateEndHandler(NULL)
, stateStore(stateStore)
, settings(settings)
, packetSender(packetSender)
, transitions(transitions)
, repeatsOverride(0)
{ }
void MiLightClient::setHeld(bool held) {
currentRemote->packetFormatter->setHeld(held);
}
void MiLightClient::prepare(
const MiLightRemoteConfig* config,
const uint16_t deviceId,
const uint8_t groupId
) {
this->currentRemote = config;
if (deviceId >= 0 && groupId >= 0) {
currentRemote->packetFormatter->prepare(deviceId, groupId);
}
this->currentState = stateStore->get(deviceId, groupId, config->type);
}
void MiLightClient::prepare(
const MiLightRemoteType type,
const uint16_t deviceId,
const uint8_t groupId
) {
prepare(MiLightRemoteConfig::fromType(type), deviceId, groupId);
}
void MiLightClient::updateColorRaw(const uint8_t color) {
#ifdef DEBUG_CLIENT_COMMANDS
Serial.printf_P(PSTR("MiLightClient::updateColorRaw: Change color to %d\n"), color);
#endif
currentRemote->packetFormatter->updateColorRaw(color);
flushPacket();
}
void MiLightClient::updateHue(const uint16_t hue) {
#ifdef DEBUG_CLIENT_COMMANDS
Serial.printf_P(PSTR("MiLightClient::updateHue: Change hue to %d\n"), hue);
#endif
currentRemote->packetFormatter->updateHue(hue);
flushPacket();
}
void MiLightClient::updateBrightness(const uint8_t brightness) {
#ifdef DEBUG_CLIENT_COMMANDS
Serial.printf_P(PSTR("MiLightClient::updateBrightness: Change brightness to %d\n"), brightness);
#endif
currentRemote->packetFormatter->updateBrightness(brightness);
flushPacket();
}
void MiLightClient::updateMode(uint8_t mode) {
#ifdef DEBUG_CLIENT_COMMANDS
Serial.printf_P(PSTR("MiLightClient::updateMode: Change mode to %d\n"), mode);
#endif
currentRemote->packetFormatter->updateMode(mode);
flushPacket();
}
void MiLightClient::nextMode() {
#ifdef DEBUG_CLIENT_COMMANDS
Serial.println(F("MiLightClient::nextMode: Switch to next mode"));
#endif
currentRemote->packetFormatter->nextMode();
flushPacket();
}
void MiLightClient::previousMode() {
#ifdef DEBUG_CLIENT_COMMANDS
Serial.println(F("MiLightClient::previousMode: Switch to previous mode"));
#endif
currentRemote->packetFormatter->previousMode();
flushPacket();
}
void MiLightClient::modeSpeedDown() {
#ifdef DEBUG_CLIENT_COMMANDS
Serial.println(F("MiLightClient::modeSpeedDown: Speed down\n"));
#endif
currentRemote->packetFormatter->modeSpeedDown();
flushPacket();
}
void MiLightClient::modeSpeedUp() {
#ifdef DEBUG_CLIENT_COMMANDS
Serial.println(F("MiLightClient::modeSpeedUp: Speed up"));
#endif
currentRemote->packetFormatter->modeSpeedUp();
flushPacket();
}
void MiLightClient::updateStatus(MiLightStatus status, uint8_t groupId) {
#ifdef DEBUG_CLIENT_COMMANDS
Serial.printf_P(PSTR("MiLightClient::updateStatus: Status %s, groupId %d\n"), status == MiLightStatus::OFF ? "OFF" : "ON", groupId);
#endif
currentRemote->packetFormatter->updateStatus(status, groupId);
flushPacket();
}
void MiLightClient::updateStatus(MiLightStatus status) {
#ifdef DEBUG_CLIENT_COMMANDS
Serial.printf_P(PSTR("MiLightClient::updateStatus: Status %s\n"), status == MiLightStatus::OFF ? "OFF" : "ON");
#endif
currentRemote->packetFormatter->updateStatus(status);
flushPacket();
}
void MiLightClient::updateSaturation(const uint8_t value) {
#ifdef DEBUG_CLIENT_COMMANDS
Serial.printf_P(PSTR("MiLightClient::updateSaturation: Saturation %d\n"), value);
#endif
currentRemote->packetFormatter->updateSaturation(value);
flushPacket();
}
void MiLightClient::updateColorWhite() {
#ifdef DEBUG_CLIENT_COMMANDS
Serial.println(F("MiLightClient::updateColorWhite: Color white"));
#endif
currentRemote->packetFormatter->updateColorWhite();
flushPacket();
}
void MiLightClient::enableNightMode() {
#ifdef DEBUG_CLIENT_COMMANDS
Serial.println(F("MiLightClient::enableNightMode: Night mode"));
#endif
currentRemote->packetFormatter->enableNightMode();
flushPacket();
}
void MiLightClient::pair() {
#ifdef DEBUG_CLIENT_COMMANDS
Serial.println(F("MiLightClient::pair: Pair"));
#endif
currentRemote->packetFormatter->pair();
flushPacket();
}
void MiLightClient::unpair() {
#ifdef DEBUG_CLIENT_COMMANDS
Serial.println(F("MiLightClient::unpair: Unpair"));
#endif
currentRemote->packetFormatter->unpair();
flushPacket();
}
void MiLightClient::increaseBrightness() {
#ifdef DEBUG_CLIENT_COMMANDS
Serial.println(F("MiLightClient::increaseBrightness: Increase brightness"));
#endif
currentRemote->packetFormatter->increaseBrightness();
flushPacket();
}
void MiLightClient::decreaseBrightness() {
#ifdef DEBUG_CLIENT_COMMANDS
Serial.println(F("MiLightClient::decreaseBrightness: Decrease brightness"));
#endif
currentRemote->packetFormatter->decreaseBrightness();
flushPacket();
}
void MiLightClient::increaseTemperature() {
#ifdef DEBUG_CLIENT_COMMANDS
Serial.println(F("MiLightClient::increaseTemperature: Increase temperature"));
#endif
currentRemote->packetFormatter->increaseTemperature();
flushPacket();
}
void MiLightClient::decreaseTemperature() {
#ifdef DEBUG_CLIENT_COMMANDS
Serial.println(F("MiLightClient::decreaseTemperature: Decrease temperature"));
#endif
currentRemote->packetFormatter->decreaseTemperature();
flushPacket();
}
void MiLightClient::updateTemperature(const uint8_t temperature) {
#ifdef DEBUG_CLIENT_COMMANDS
Serial.printf_P(PSTR("MiLightClient::updateTemperature: Set temperature to %d\n"), temperature);
#endif
currentRemote->packetFormatter->updateTemperature(temperature);
flushPacket();
}
void MiLightClient::command(uint8_t command, uint8_t arg) {
#ifdef DEBUG_CLIENT_COMMANDS
Serial.printf_P(PSTR("MiLightClient::command: Execute command %d, argument %d\n"), command, arg);
#endif
currentRemote->packetFormatter->command(command, arg);
flushPacket();
}
void MiLightClient::toggleStatus() {
#ifdef DEBUG_CLIENT_COMMANDS
Serial.printf_P(PSTR("MiLightClient::toggleStatus"));
#endif
currentRemote->packetFormatter->toggleStatus();
flushPacket();
}
void MiLightClient::updateColor(JsonVariant json) {
ParsedColor color = ParsedColor::fromJson(json);
if (!color.success) {
Serial.println(F("Error parsing color field, unrecognized format"));
return;
}
// We consider an RGB color "white" if all color intensities are roughly the
// same value. An unscientific value of 10 (~4%) is chosen.
if ( abs(color.r - color.g) < RGB_WHITE_THRESHOLD
&& abs(color.g - color.b) < RGB_WHITE_THRESHOLD
&& abs(color.r - color.b) < RGB_WHITE_THRESHOLD) {
this->updateColorWhite();
} else {
this->updateHue(color.hue);
this->updateSaturation(color.saturation);
}
}
void MiLightClient::update(JsonObject request) {
if (this->updateBeginHandler) {
this->updateBeginHandler();
}
const JsonVariant status = this->extractStatus(request);
const uint8_t parsedStatus = this->parseStatus(status);
const JsonVariant jsonTransition = request[RequestKeys::TRANSITION];
float transition = 0;
if (!jsonTransition.isNull()) {
if (jsonTransition.is<float>()) {
transition = jsonTransition.as<float>();
} else if (jsonTransition.is<size_t>()) {
transition = jsonTransition.as<size_t>();
} else {
Serial.println(F("MiLightClient - WARN: unsupported transition type. Must be float or int."));
}
}
JsonVariant brightness = request[GroupStateFieldNames::BRIGHTNESS];
JsonVariant level = request[GroupStateFieldNames::LEVEL];
const bool isBrightnessDefined = !brightness.isUndefined() || !level.isUndefined();
// Always turn on first
if (parsedStatus == ON) {
if (transition == 0) {
this->updateStatus(ON);
}
// Don't do an "On" transition if the bulb is already on. The reasons for this are:
// * Ambiguous what the behavior should be. Should it ramp to full brightness?
// * HomeAssistant is only capable of sending transitions via the `light.turn_on`
// service call, which ends up sending `{"status":"ON"}`. So transitions which
// have nothing to do with the status will include an "ON" command.
// If the user wants to transition brightness, they can just specify a brightness in
// the same command. This avoids the need to make arbitrary calls on what the
// behavior should be.
else if (!currentState->isSetState() || !currentState->isOn()) {
// If a brightness is defined, we'll want to transition to that. Status
// transitions only ramp up/down to the max/min. Otherwise, just turn the bulb on
// and let field transitions handle the rest.
if (!isBrightnessDefined) {
handleTransition(GroupStateField::STATUS, status, transition, 0);
} else {
this->updateStatus(ON);
if (! brightness.isUndefined()) {
handleTransition(GroupStateField::BRIGHTNESS, brightness, transition, 0);
} else if (! level.isUndefined()) {
handleTransition(GroupStateField::LEVEL, level, transition, 0);
}
}
}
}
for (const char* fieldName : FIELD_ORDERINGS) {
if (request.containsKey(fieldName)) {
auto handler = FIELD_SETTERS.find(fieldName);
JsonVariant value = request[fieldName];
if (handler != FIELD_SETTERS.end()) {
// No transition -- set field directly
if (transition == 0) {
handler->second(this, value);
} else {
GroupStateField field = GroupStateFieldHelpers::getFieldByName(fieldName);
if ( !GroupStateFieldHelpers::isBrightnessField(field) // If field isn't brightness
|| parsedStatus == STATUS_UNDEFINED // or if there was not a status field
|| currentState->isOn() // or if bulb was already on
) {
handleTransition(field, value, transition);
}
}
}
}
}
// Raw packet command/args
if (request.containsKey("button_id") && request.containsKey("argument")) {
this->command(request["button_id"], request["argument"]);
}
// Always turn off last
if (parsedStatus == OFF) {
if (transition == 0) {
this->updateStatus(OFF);
} else {
handleTransition(GroupStateField::STATUS, status, transition);
}
}
if (this->updateEndHandler) {
this->updateEndHandler();
}
}
void MiLightClient::handleCommands(JsonArray commands) {
if (! commands.isNull()) {
for (size_t i = 0; i < commands.size(); i++) {
this->handleCommand(commands[i]);
}
}
}
void MiLightClient::handleCommand(JsonVariant command) {
String cmdName;
JsonObject args;
if (command.is<JsonObject>()) {
JsonObject cmdObj = command.as<JsonObject>();
cmdName = cmdObj[GroupStateFieldNames::COMMAND].as<const char*>();
args = cmdObj["args"];
} else if (command.is<const char*>()) {
cmdName = command.as<const char*>();
}
if (cmdName == MiLightCommandNames::UNPAIR) {
this->unpair();
} else if (cmdName == MiLightCommandNames::PAIR) {
this->pair();
} else if (cmdName == MiLightCommandNames::SET_WHITE) {
this->updateColorWhite();
} else if (cmdName == MiLightCommandNames::NIGHT_MODE) {
this->enableNightMode();
} else if (cmdName == MiLightCommandNames::LEVEL_UP) {
this->increaseBrightness();
} else if (cmdName == MiLightCommandNames::LEVEL_DOWN) {
this->decreaseBrightness();
} else if (cmdName == "brightness_up") {
this->increaseBrightness();
} else if (cmdName == "brightness_down") {
this->decreaseBrightness();
} else if (cmdName == MiLightCommandNames::TEMPERATURE_UP) {
this->increaseTemperature();
} else if (cmdName == MiLightCommandNames::TEMPERATURE_DOWN) {
this->decreaseTemperature();
} else if (cmdName == MiLightCommandNames::NEXT_MODE) {
this->nextMode();
} else if (cmdName == MiLightCommandNames::PREVIOUS_MODE) {
this->previousMode();
} else if (cmdName == MiLightCommandNames::MODE_SPEED_DOWN) {
this->modeSpeedDown();
} else if (cmdName == MiLightCommandNames::MODE_SPEED_UP) {
this->modeSpeedUp();
} else if (cmdName == MiLightCommandNames::TOGGLE) {
this->toggleStatus();
} else if (cmdName == MiLightCommandNames::TRANSITION) {
StaticJsonDocument<100> fakedoc;
this->handleTransition(args, fakedoc);
}
}
void MiLightClient::handleTransition(GroupStateField field, JsonVariant value, float duration, int16_t startValue) {
BulbId bulbId = currentRemote->packetFormatter->currentBulbId();
std::shared_ptr<Transition::Builder> transitionBuilder = nullptr;
if (currentState == nullptr) {
Serial.println(F("Error planning transition: could not find current bulb state."));
return;
}
if (!currentState->isSetField(field)) {
Serial.println(F("Error planning transition: current state for field could not be determined"));
return;
}
if (field == GroupStateField::COLOR) {
ParsedColor currentColor = currentState->getColor();
ParsedColor endColor = ParsedColor::fromJson(value);
transitionBuilder = transitions.buildColorTransition(
bulbId,
currentColor,
endColor
);
} else if (field == GroupStateField::STATUS || field == GroupStateField::STATE) {
uint8_t startLevel;
MiLightStatus status = parseMilightStatus(value);
if (startValue == FETCH_VALUE_FROM_STATE || currentState->isOn()) {
startLevel = currentState->getBrightness();
} else {
startLevel = startValue;
}
transitionBuilder = transitions.buildStatusTransition(bulbId, status, startLevel);
} else {
uint16_t currentValue;
uint16_t endValue = value;
if (startValue == FETCH_VALUE_FROM_STATE || currentState->isOn()) {
currentValue = currentState->getParsedFieldValue(field);
} else {
currentValue = startValue;
}
transitionBuilder = transitions.buildFieldTransition(
bulbId,
field,
currentValue,
endValue
);
}
if (transitionBuilder == nullptr) {
Serial.printf_P(PSTR("Unsupported transition field: %s\n"), GroupStateFieldHelpers::getFieldName(field));
return;
}
transitionBuilder->setDuration(duration);
transitions.addTransition(transitionBuilder->build());
}
bool MiLightClient::handleTransition(JsonObject args, JsonDocument& responseObj) {
if (! args.containsKey(FS(TransitionParams::FIELD))
|| ! args.containsKey(FS(TransitionParams::END_VALUE))) {
responseObj[F("error")] = F("Ignoring transition missing required arguments");
return false;
}
const BulbId& bulbId = currentRemote->packetFormatter->currentBulbId();
const char* fieldName = args[FS(TransitionParams::FIELD)];
JsonVariant startValue = args[FS(TransitionParams::START_VALUE)];
JsonVariant endValue = args[FS(TransitionParams::END_VALUE)];
GroupStateField field = GroupStateFieldHelpers::getFieldByName(fieldName);
std::shared_ptr<Transition::Builder> transitionBuilder = nullptr;
if (field == GroupStateField::UNKNOWN) {
char errorMsg[30];
sprintf_P(errorMsg, PSTR("Unknown transition field: %s\n"), fieldName);
responseObj[F("error")] = errorMsg;
return false;
}
// These fields can be transitioned directly.
switch (field) {
case GroupStateField::HUE:
case GroupStateField::SATURATION:
case GroupStateField::BRIGHTNESS:
case GroupStateField::LEVEL:
case GroupStateField::KELVIN:
case GroupStateField::COLOR_TEMP:
transitionBuilder = transitions.buildFieldTransition(
bulbId,
field,
startValue.isUndefined()
? currentState->getParsedFieldValue(field)
: startValue.as<uint16_t>(),
endValue
);
break;
default:
break;
}
// Color can be decomposed into hue/saturation and these can be transitioned separately
if (field == GroupStateField::COLOR) {
ParsedColor _startValue = startValue.isUndefined()
? currentState->getColor()
: ParsedColor::fromJson(startValue);
ParsedColor endColor = ParsedColor::fromJson(endValue);
if (! _startValue.success) {
responseObj[F("error")] = F("Transition - error parsing start color");
return false;
}
if (! endColor.success) {
responseObj[F("error")] = F("Transition - error parsing end color");
return false;
}
transitionBuilder = transitions.buildColorTransition(
bulbId,
_startValue,
endColor
);
}
// Status is handled a little differently
if (field == GroupStateField::STATUS || field == GroupStateField::STATE) {
MiLightStatus toStatus = parseMilightStatus(endValue);
uint8_t startLevel;
if (currentState->isSetBrightness()) {
startLevel = currentState->getBrightness();
} else if (toStatus == ON) {
startLevel = 0;
} else {
startLevel = 100;
}
transitionBuilder = transitions.buildStatusTransition(bulbId, toStatus, startLevel);
}
if (transitionBuilder == nullptr) {
char errorMsg[30];
sprintf_P(errorMsg, PSTR("Recognized, but unsupported transition field: %s\n"), fieldName);
responseObj[F("error")] = errorMsg;
return false;
}
if (args.containsKey(FS(TransitionParams::DURATION))) {
transitionBuilder->setDuration(args[FS(TransitionParams::DURATION)]);
}
if (args.containsKey(FS(TransitionParams::PERIOD))) {
transitionBuilder->setPeriod(args[FS(TransitionParams::PERIOD)]);
}
transitions.addTransition(transitionBuilder->build());
return true;
}
void MiLightClient::handleEffect(const String& effect) {
if (effect == MiLightCommandNames::NIGHT_MODE) {
this->enableNightMode();
} else if (effect == "white" || effect == "white_mode") {
this->updateColorWhite();
} else { // assume we're trying to set mode
this->updateMode(effect.toInt());
}
}
JsonVariant MiLightClient::extractStatus(JsonObject object) {
JsonVariant status;
if (object.containsKey(FS(GroupStateFieldNames::STATUS))) {
return object[FS(GroupStateFieldNames::STATUS)];
} else {
return object[FS(GroupStateFieldNames::STATE)];
}
}
uint8_t MiLightClient::parseStatus(JsonVariant val) {
if (val.isUndefined()) {
return STATUS_UNDEFINED;
}
return parseMilightStatus(val);
}
void MiLightClient::setRepeatsOverride(size_t repeats) {
this->repeatsOverride = repeats;
}
void MiLightClient::clearRepeatsOverride() {
this->repeatsOverride = PacketSender::DEFAULT_PACKET_SENDS_VALUE;
}
void MiLightClient::flushPacket() {
PacketStream& stream = currentRemote->packetFormatter->buildPackets();
while (stream.hasNext()) {
packetSender.enqueue(stream.next(), currentRemote, repeatsOverride);
}
currentRemote->packetFormatter->reset();
}
void MiLightClient::onUpdateBegin(EventHandler handler) {
this->updateBeginHandler = handler;
}
void MiLightClient::onUpdateEnd(EventHandler handler) {
this->updateEndHandler = handler;
}