esp32_ethernet_milight_hub/lib/MiLightState/GroupState.cpp

1024 lines
29 KiB
C++

#include <GroupState.h>
#include <Units.h>
#include <MiLightRemoteConfig.h>
#include <RGBConverter.h>
#include <BulbId.h>
#include <MiLightCommands.h>
static const char* BULB_MODE_NAMES[] = {
"white",
"color",
"scene",
"night"
};
const BulbId DEFAULT_BULB_ID;
const GroupStateField GroupState::ALL_PHYSICAL_FIELDS[] = {
GroupStateField::BULB_MODE,
GroupStateField::HUE,
GroupStateField::KELVIN,
GroupStateField::MODE,
GroupStateField::SATURATION,
GroupStateField::STATE,
GroupStateField::BRIGHTNESS
};
static const GroupStateField ALL_SCRATCH_FIELDS[] = {
GroupStateField::BRIGHTNESS,
GroupStateField::KELVIN
};
// Number of units each increment command counts for
static const uint8_t INCREMENT_COMMAND_VALUE = 10;
static const GroupState DEFAULT_STATE = GroupState();
static const GroupState DEFAULT_RGB_ONLY_STATE = GroupState::initDefaultRgbState();
static const GroupState DEFAULT_WHITE_ONLY_STATE = GroupState::initDefaultWhiteState();
GroupState GroupState::initDefaultRgbState() {
GroupState state;
state.setBulbMode(BULB_MODE_COLOR);
return state;
}
GroupState GroupState::initDefaultWhiteState() {
GroupState state;
state.setBulbMode(BULB_MODE_WHITE);
return state;
}
const GroupState& GroupState::defaultState(MiLightRemoteType remoteType) {
switch (remoteType) {
case REMOTE_TYPE_RGB:
return DEFAULT_RGB_ONLY_STATE;
break;
case REMOTE_TYPE_CCT:
case REMOTE_TYPE_FUT091:
return DEFAULT_WHITE_ONLY_STATE;
break;
default:
// No modifications needed
break;
}
return DEFAULT_STATE;
}
void GroupState::initFields() {
state.fields._state = 0;
state.fields._brightness = 0;
state.fields._brightnessColor = 0;
state.fields._brightnessMode = 0;
state.fields._hue = 0;
state.fields._saturation = 0;
state.fields._mode = 0;
state.fields._bulbMode = 0;
state.fields._kelvin = 0;
state.fields._isSetState = 0;
state.fields._isSetHue = 0;
state.fields._isSetBrightness = 0;
state.fields._isSetBrightnessColor = 0;
state.fields._isSetBrightnessMode = 0;
state.fields._isSetSaturation = 0;
state.fields._isSetMode = 0;
state.fields._isSetKelvin = 0;
state.fields._isSetBulbMode = 0;
state.fields._dirty = 1;
state.fields._mqttDirty = 0;
state.fields._isSetNightMode = 0;
state.fields._isNightMode = 0;
scratchpad.fields._isSetBrightnessScratch = 0;
scratchpad.fields._brightnessScratch = 0;
scratchpad.fields._isSetKelvinScratch = 0;
scratchpad.fields._kelvinScratch = 0;
}
GroupState& GroupState::operator=(const GroupState& other) {
memcpy(state.rawData, other.state.rawData, DATA_LONGS * sizeof(uint32_t));
scratchpad.rawData = other.scratchpad.rawData;
return *this;
}
GroupState::GroupState()
: previousState(NULL)
{
initFields();
}
GroupState::GroupState(const GroupState& other)
: previousState(NULL)
{
memcpy(state.rawData, other.state.rawData, DATA_LONGS * sizeof(uint32_t));
scratchpad.rawData = other.scratchpad.rawData;
}
GroupState::GroupState(const GroupState* previousState, JsonObject jsonState)
: previousState(previousState)
{
initFields();
if (previousState != NULL) {
this->scratchpad = previousState->scratchpad;
}
patch(jsonState);
}
bool GroupState::operator==(const GroupState& other) const {
return memcmp(state.rawData, other.state.rawData, DATA_LONGS * sizeof(uint32_t)) == 0;
}
bool GroupState::isEqualIgnoreDirty(const GroupState& other) const {
GroupState meCopy = *this;
GroupState otherCopy = other;
meCopy.clearDirty();
meCopy.clearMqttDirty();
otherCopy.clearDirty();
otherCopy.clearMqttDirty();
return meCopy == otherCopy;
}
void GroupState::print(Stream& stream) const {
stream.printf("State: %08X %08X\n", state.rawData[0], state.rawData[1]);
}
bool GroupState::clearField(GroupStateField field) {
bool clearedAny = false;
switch (field) {
// Always set and can't be cleared
case GroupStateField::COMPUTED_COLOR:
case GroupStateField::DEVICE_ID:
case GroupStateField::GROUP_ID:
case GroupStateField::DEVICE_TYPE:
break;
case GroupStateField::STATE:
case GroupStateField::STATUS:
clearedAny = isSetState();
state.fields._isSetState = 0;
break;
case GroupStateField::BRIGHTNESS:
case GroupStateField::LEVEL:
clearedAny = clearBrightness();
break;
case GroupStateField::COLOR:
case GroupStateField::HUE:
case GroupStateField::OH_COLOR:
case GroupStateField::HEX_COLOR:
clearedAny = isSetHue();
state.fields._isSetHue = 0;
break;
case GroupStateField::SATURATION:
clearedAny = isSetSaturation();
state.fields._isSetSaturation = 0;
break;
case GroupStateField::MODE:
case GroupStateField::EFFECT:
clearedAny = isSetMode();
state.fields._isSetMode = 0;
break;
case GroupStateField::KELVIN:
case GroupStateField::COLOR_TEMP:
clearedAny = isSetKelvin();
state.fields._isSetKelvin = 0;
break;
case GroupStateField::BULB_MODE:
clearedAny = isSetBulbMode();
state.fields._isSetBulbMode = 0;
// Clear brightness as well
clearedAny = clearBrightness() || clearedAny;
break;
default:
Serial.printf_P(PSTR("Attempted to clear unknown field: %d\n"), static_cast<uint8_t>(field));
break;
}
return clearedAny;
}
bool GroupState::isSetField(GroupStateField field) const {
switch (field) {
case GroupStateField::COMPUTED_COLOR:
// Always set -- either send RGB color or white
return true;
case GroupStateField::DEVICE_ID:
case GroupStateField::GROUP_ID:
case GroupStateField::DEVICE_TYPE:
// These are always defined
return true;
case GroupStateField::STATE:
case GroupStateField::STATUS:
return isSetState();
case GroupStateField::BRIGHTNESS:
case GroupStateField::LEVEL:
return isSetBrightness();
case GroupStateField::COLOR:
case GroupStateField::HUE:
case GroupStateField::OH_COLOR:
case GroupStateField::HEX_COLOR:
return isSetHue();
case GroupStateField::SATURATION:
return isSetSaturation();
case GroupStateField::MODE:
return isSetMode();
case GroupStateField::EFFECT:
return isSetEffect();
case GroupStateField::KELVIN:
case GroupStateField::COLOR_TEMP:
return isSetKelvin();
case GroupStateField::BULB_MODE:
return isSetBulbMode();
default:
Serial.print(F("WARNING: tried to check if unknown field was set: "));
Serial.println(static_cast<unsigned int>(field));
break;
}
return false;
}
bool GroupState::isSetScratchField(GroupStateField field) const {
switch (field) {
case GroupStateField::BRIGHTNESS:
return scratchpad.fields._isSetBrightnessScratch;
case GroupStateField::KELVIN:
return scratchpad.fields._isSetKelvinScratch;
default:
Serial.print(F("WARNING: tried to check if unknown scratch field was set: "));
Serial.println(static_cast<unsigned int>(field));
break;
}
return false;
}
uint16_t GroupState::getFieldValue(GroupStateField field) const {
switch (field) {
case GroupStateField::STATE:
case GroupStateField::STATUS:
return getState();
case GroupStateField::BRIGHTNESS:
return getBrightness();
case GroupStateField::HUE:
return getHue();
case GroupStateField::SATURATION:
return getSaturation();
case GroupStateField::MODE:
return getMode();
case GroupStateField::KELVIN:
return getKelvin();
case GroupStateField::BULB_MODE:
return getBulbMode();
default:
Serial.print(F("WARNING: tried to fetch value for unknown field: "));
Serial.println(static_cast<unsigned int>(field));
break;
}
return 0;
}
uint16_t GroupState::getParsedFieldValue(GroupStateField field) const {
switch (field) {
case GroupStateField::LEVEL:
return getBrightness();
case GroupStateField::BRIGHTNESS:
return Units::rescale(getBrightness(), 255, 100);
case GroupStateField::COLOR_TEMP:
return getMireds();
default:
return getFieldValue(field);
}
}
uint16_t GroupState::getScratchFieldValue(GroupStateField field) const {
switch (field) {
case GroupStateField::BRIGHTNESS:
return scratchpad.fields._brightnessScratch;
case GroupStateField::KELVIN:
return scratchpad.fields._kelvinScratch;
default:
Serial.print(F("WARNING: tried to fetch value for unknown scratch field: "));
Serial.println(static_cast<unsigned int>(field));
break;
}
return 0;
}
void GroupState::setFieldValue(GroupStateField field, uint16_t value) {
switch (field) {
case GroupStateField::STATE:
case GroupStateField::STATUS:
setState(static_cast<MiLightStatus>(value));
break;
case GroupStateField::BRIGHTNESS:
setBrightness(value);
break;
case GroupStateField::HUE:
setHue(value);
break;
case GroupStateField::SATURATION:
setSaturation(value);
break;
case GroupStateField::MODE:
setMode(value);
break;
case GroupStateField::KELVIN:
setKelvin(value);
break;
case GroupStateField::BULB_MODE:
setBulbMode(static_cast<BulbMode>(value));
break;
default:
Serial.print(F("WARNING: tried to set value for unknown field: "));
Serial.println(static_cast<unsigned int>(field));
break;
}
}
void GroupState::setScratchFieldValue(GroupStateField field, uint16_t value) {
switch (field) {
case GroupStateField::BRIGHTNESS:
scratchpad.fields._isSetBrightnessScratch = 1;
scratchpad.fields._brightnessScratch = value;
break;
case GroupStateField::KELVIN:
scratchpad.fields._isSetKelvinScratch = 1;
scratchpad.fields._kelvinScratch = value;
break;
default:
Serial.print(F("WARNING: tried to set value for unknown scratch field: "));
Serial.println(static_cast<unsigned int>(field));
break;
}
}
bool GroupState::isSetState() const { return state.fields._isSetState; }
MiLightStatus GroupState::getState() const { return state.fields._state ? ON : OFF; }
bool GroupState::isOn() const {
return !isNightMode() && (!isSetState() || getState() == MiLightStatus::ON);
}
bool GroupState::setState(const MiLightStatus status) {
if (!isNightMode() && isSetState() && getState() == status) {
return false;
}
setDirty();
state.fields._isSetState = 1;
state.fields._state = status == ON ? 1 : 0;
// Changing status will clear night mode
setNightMode(false);
return true;
}
bool GroupState::isSetBrightness() const {
// If we don't know what mode we're in, just assume white mode. Do this for a few
// reasons:
// * Some bulbs don't have multiple modes
// * It's confusing to not have a default
if (! isSetBulbMode()) {
return state.fields._isSetBrightness;
}
switch (state.fields._bulbMode) {
case BULB_MODE_WHITE:
return state.fields._isSetBrightness;
case BULB_MODE_COLOR:
return state.fields._isSetBrightnessColor;
case BULB_MODE_SCENE:
return state.fields._isSetBrightnessMode;
}
return false;
}
bool GroupState::clearBrightness() {
bool cleared = false;
if (!state.fields._isSetBulbMode) {
cleared = state.fields._isSetBrightness;
state.fields._isSetBrightness = 0;
} else {
switch (state.fields._bulbMode) {
case BULB_MODE_COLOR:
cleared = state.fields._isSetBrightnessColor;
state.fields._isSetBrightnessColor = 0;
break;
case BULB_MODE_SCENE:
cleared = state.fields._isSetBrightnessMode;
state.fields._isSetBrightnessMode = 0;
break;
case BULB_MODE_WHITE:
cleared = state.fields._isSetBrightness;
state.fields._isSetBrightness = 0;
break;
}
}
return cleared;
}
uint8_t GroupState::getBrightness() const {
switch (state.fields._bulbMode) {
case BULB_MODE_WHITE:
return state.fields._brightness;
case BULB_MODE_COLOR:
return state.fields._brightnessColor;
case BULB_MODE_SCENE:
return state.fields._brightnessMode;
}
return 0;
}
bool GroupState::setBrightness(uint8_t brightness) {
if (isSetBrightness() && getBrightness() == brightness) {
return false;
}
setDirty();
uint8_t bulbMode = state.fields._bulbMode;
if (! state.fields._isSetBulbMode) {
bulbMode = BULB_MODE_WHITE;
}
switch (bulbMode) {
case BULB_MODE_WHITE:
state.fields._isSetBrightness = 1;
state.fields._brightness = brightness;
break;
case BULB_MODE_COLOR:
state.fields._isSetBrightnessColor = 1;
state.fields._brightnessColor = brightness;
break;
case BULB_MODE_SCENE:
state.fields._isSetBrightnessMode = 1;
state.fields._brightnessMode = brightness;
default:
return false;
}
return true;
}
bool GroupState::isSetHue() const { return state.fields._isSetHue; }
uint16_t GroupState::getHue() const {
return Units::rescale<uint16_t, uint16_t>(state.fields._hue, 360, 255);
}
bool GroupState::setHue(uint16_t hue) {
if (isSetHue() && getHue() == hue) {
return false;
}
setDirty();
state.fields._isSetHue = 1;
state.fields._hue = Units::rescale<uint16_t, uint16_t>(hue, 255, 360);
return true;
}
bool GroupState::isSetSaturation() const { return state.fields._isSetSaturation; }
uint8_t GroupState::getSaturation() const { return state.fields._saturation; }
bool GroupState::setSaturation(uint8_t saturation) {
if (isSetSaturation() && getSaturation() == saturation) {
return false;
}
setDirty();
state.fields._isSetSaturation = 1;
state.fields._saturation = saturation;
return true;
}
bool GroupState::isSetMode() const { return state.fields._isSetMode; }
bool GroupState::isSetEffect() const {
// only BULB_MODE_COLOR does not have an effect.
return isSetBulbMode() && getBulbMode() != BULB_MODE_COLOR;
}
uint8_t GroupState::getMode() const { return state.fields._mode; }
bool GroupState::setMode(uint8_t mode) {
if (isSetMode() && getMode() == mode) {
return false;
}
setDirty();
state.fields._isSetMode = 1;
state.fields._mode = mode;
return true;
}
bool GroupState::isSetKelvin() const { return state.fields._isSetKelvin; }
uint8_t GroupState::getKelvin() const { return state.fields._kelvin; }
uint16_t GroupState::getMireds() const {
return Units::whiteValToMireds(getKelvin(), 100);
}
bool GroupState::setKelvin(uint8_t kelvin) {
if (isSetKelvin() && getKelvin() == kelvin) {
return false;
}
setDirty();
state.fields._isSetKelvin = 1;
state.fields._kelvin = kelvin;
return true;
}
bool GroupState::setMireds(uint16_t mireds) {
return setKelvin(Units::miredsToWhiteVal(mireds, 100));
}
bool GroupState::isSetBulbMode() const {
return (isSetNightMode() && isNightMode()) || state.fields._isSetBulbMode;
}
BulbMode GroupState::getBulbMode() const {
// Night mode is a transient state. When power is toggled, the bulb returns
// to the state it was last in. To handle this case, night mode state is
// stored separately.
if (isSetNightMode() && isNightMode()) {
return BULB_MODE_NIGHT;
} else {
return static_cast<BulbMode>(state.fields._bulbMode);
}
}
bool GroupState::setBulbMode(BulbMode bulbMode) {
if (isSetBulbMode() && getBulbMode() == bulbMode) {
return false;
}
setDirty();
// As mentioned in isSetBulbMode, NIGHT_MODE is stored separately.
if (bulbMode == BULB_MODE_NIGHT) {
setNightMode(true);
} else {
state.fields._isSetBulbMode = 1;
state.fields._bulbMode = bulbMode;
}
return true;
}
bool GroupState::isSetNightMode() const { return state.fields._isSetNightMode; }
bool GroupState::isNightMode() const { return state.fields._isNightMode; }
bool GroupState::setNightMode(bool nightMode) {
if (isSetNightMode() && isNightMode() == nightMode) {
return false;
}
setDirty();
state.fields._isSetNightMode = 1;
state.fields._isNightMode = nightMode;
return true;
}
bool GroupState::isDirty() const { return state.fields._dirty; }
inline bool GroupState::setDirty() {
state.fields._dirty = 1;
state.fields._mqttDirty = 1;
return true;
}
bool GroupState::clearDirty() {
state.fields._dirty = 0;
return true;
}
bool GroupState::isMqttDirty() const { return state.fields._mqttDirty; }
bool GroupState::clearMqttDirty() {
state.fields._mqttDirty = 0;
return true;
}
void GroupState::load(Stream& stream) {
for (size_t i = 0; i < DATA_LONGS; i++) {
stream.readBytes(reinterpret_cast<uint8_t*>(&state.rawData[i]), 4);
}
clearDirty();
}
void GroupState::dump(Stream& stream) const {
for (size_t i = 0; i < DATA_LONGS; i++) {
stream.write(reinterpret_cast<const uint8_t*>(&state.rawData[i]), 4);
}
}
bool GroupState::applyIncrementCommand(GroupStateField field, IncrementDirection dir) {
if (field != GroupStateField::KELVIN && field != GroupStateField::BRIGHTNESS) {
Serial.print(F("WARNING: tried to apply increment for unsupported field: "));
Serial.println(static_cast<uint8_t>(field));
return false;
}
int8_t dirValue = static_cast<int8_t>(dir);
// If there's already a known value, update it
if (previousState != NULL && previousState->isSetField(field)) {
int8_t currentValue = static_cast<int8_t>(previousState->getFieldValue(field));
int8_t newValue = currentValue + (dirValue * INCREMENT_COMMAND_VALUE);
#ifdef STATE_DEBUG
previousState->debugState("Updating field from increment command");
#endif
// For now, assume range for both brightness and kelvin is [0, 100]
setFieldValue(field, constrain(newValue, 0, 100));
return true;
// Otherwise start or update scratch state
} else {
if (isSetScratchField(field)) {
int8_t newValue = static_cast<int8_t>(getScratchFieldValue(field)) + dirValue;
if (newValue == 0 || newValue == 10) {
setFieldValue(field, newValue * INCREMENT_COMMAND_VALUE);
return true;
} else {
setScratchFieldValue(field, newValue);
}
} else if (dir == IncrementDirection::DECREASE) {
setScratchFieldValue(field, 9);
} else {
setScratchFieldValue(field, 1);
}
#ifdef STATE_DEBUG
Serial.print(F("Updated scratch field: "));
Serial.print(static_cast<int8_t>(field));
Serial.print(F(" to: "));
Serial.println(getScratchFieldValue(field));
#endif
}
return false;
}
bool GroupState::clearNonMatchingFields(const GroupState& other) {
#ifdef STATE_DEBUG
this->debugState("Clearing fields. Current state");
other.debugState("Other state");
#endif
bool clearedAny = false;
for (size_t i = 0; i < size(ALL_PHYSICAL_FIELDS); ++i) {
GroupStateField field = ALL_PHYSICAL_FIELDS[i];
if (other.isSetField(field) && isSetField(field) && getFieldValue(field) != other.getFieldValue(field)) {
if (clearField(field)) {
clearedAny = true;
}
}
}
#ifdef STATE_DEBUG
this->debugState("Result");
#endif
return clearedAny;
}
void GroupState::patch(const GroupState& other) {
#ifdef STATE_DEBUG
other.debugState("Patching existing state with: ");
Serial.println();
#endif
for (size_t i = 0; i < size(ALL_PHYSICAL_FIELDS); ++i) {
GroupStateField field = ALL_PHYSICAL_FIELDS[i];
// Handle night mode separately. Should always set this field.
if (field == GroupStateField::BULB_MODE && other.isNightMode()) {
setFieldValue(field, other.getFieldValue(field));
}
// Otherwise...
// Conditions:
// * Only set anything if field is set in other state
// * Do not patch anything other than STATE if bulb is off
else if (other.isSetField(field) && (field == GroupStateField::STATE || isOn())) {
setFieldValue(field, other.getFieldValue(field));
}
}
for (size_t i = 0; i < size(ALL_SCRATCH_FIELDS); ++i) {
GroupStateField field = ALL_SCRATCH_FIELDS[i];
// All scratch field updates require that the bulb is on.
if (isOn() && other.isSetScratchField(field)) {
setScratchFieldValue(field, other.getScratchFieldValue(field));
}
}
}
/*
Update group state to reflect a packet state
Called both when a packet is sent locally, and when an intercepted packet is read
(see main.cpp onPacketSentHandler)
Returns true if the packet changes affects a state change
*/
bool GroupState::patch(JsonObject state) {
bool changes = false;
#ifdef STATE_DEBUG
Serial.print(F("Patching existing state with: "));
serializeJson(state, Serial);
Serial.println();
#endif
if (state.containsKey(GroupStateFieldNames::STATE)) {
bool stateChange = setState(state[GroupStateFieldNames::STATE] == "ON" ? ON : OFF);
changes |= stateChange;
}
// Devices do not support changing their state while off, so don't apply state
// changes to devices we know are off.
if (isOn() && state.containsKey(GroupStateFieldNames::BRIGHTNESS)) {
bool stateChange = setBrightness(Units::rescale(state[GroupStateFieldNames::BRIGHTNESS].as<uint8_t>(), 100, 255));
changes |= stateChange;
}
if (isOn() && state.containsKey(GroupStateFieldNames::HUE)) {
changes |= setHue(state[GroupStateFieldNames::HUE]);
changes |= setBulbMode(BULB_MODE_COLOR);
}
if (isOn() && state.containsKey(GroupStateFieldNames::SATURATION)) {
changes |= setSaturation(state[GroupStateFieldNames::SATURATION]);
}
if (isOn() && state.containsKey(GroupStateFieldNames::MODE)) {
changes |= setMode(state[GroupStateFieldNames::MODE]);
changes |= setBulbMode(BULB_MODE_SCENE);
}
if (isOn() && state.containsKey(GroupStateFieldNames::COLOR_TEMP)) {
changes |= setMireds(state[GroupStateFieldNames::COLOR_TEMP]);
changes |= setBulbMode(BULB_MODE_WHITE);
}
if (state.containsKey(GroupStateFieldNames::COMMAND)) {
const String& command = state[GroupStateFieldNames::COMMAND];
if (isOn() && command == MiLightCommandNames::SET_WHITE) {
changes |= setBulbMode(BULB_MODE_WHITE);
} else if (command == MiLightCommandNames::NIGHT_MODE) {
changes |= setBulbMode(BULB_MODE_NIGHT);
} else if (isOn() && command == "brightness_up") {
changes |= applyIncrementCommand(GroupStateField::BRIGHTNESS, IncrementDirection::INCREASE);
} else if (isOn() && command == "brightness_down") {
changes |= applyIncrementCommand(GroupStateField::BRIGHTNESS, IncrementDirection::DECREASE);
} else if (isOn() && command == MiLightCommandNames::TEMPERATURE_UP) {
changes |= applyIncrementCommand(GroupStateField::KELVIN, IncrementDirection::INCREASE);
changes |= setBulbMode(BULB_MODE_WHITE);
} else if (isOn() && command == MiLightCommandNames::TEMPERATURE_DOWN) {
changes |= applyIncrementCommand(GroupStateField::KELVIN, IncrementDirection::DECREASE);
changes |= setBulbMode(BULB_MODE_WHITE);
}
}
if (changes) {
debugState("GroupState::patch: State changed");
}
else {
debugState("GroupState::patch: State not changed");
}
return changes;
}
void GroupState::applyColor(JsonObject state) const {
ParsedColor color = getColor();
applyColor(state, color.r, color.g, color.b);
}
void GroupState::applyColor(JsonObject state, uint8_t r, uint8_t g, uint8_t b) const {
JsonObject color = state.createNestedObject(GroupStateFieldNames::COLOR);
color["r"] = r;
color["g"] = g;
color["b"] = b;
}
void GroupState::applyOhColor(JsonObject state) const {
ParsedColor color = getColor();
char ohColorStr[13];
sprintf(ohColorStr, "%d,%d,%d", color.r, color.g, color.b);
state[GroupStateFieldNames::COLOR] = ohColorStr;
}
void GroupState::applyHexColor(JsonObject state) const {
ParsedColor color = getColor();
char hexColor[8];
sprintf(hexColor, "#%02X%02X%02X", color.r, color.g, color.b);
state[GroupStateFieldNames::COLOR] = hexColor;
}
// gather partial state for a single field; see GroupState::applyState to gather many fields
void GroupState::applyField(JsonObject partialState, const BulbId& bulbId, GroupStateField field) const {
if (isSetField(field)) {
switch (field) {
case GroupStateField::STATE:
case GroupStateField::STATUS:
partialState[GroupStateFieldHelpers::getFieldName(field)] = getState() == ON ? "ON" : "OFF";
break;
case GroupStateField::BRIGHTNESS:
partialState[GroupStateFieldNames::BRIGHTNESS] = Units::rescale(getBrightness(), 255, 100);
break;
case GroupStateField::LEVEL:
partialState[GroupStateFieldNames::LEVEL] = getBrightness();
break;
case GroupStateField::BULB_MODE:
partialState[GroupStateFieldNames::BULB_MODE] = BULB_MODE_NAMES[getBulbMode()];
break;
case GroupStateField::COLOR:
if (getBulbMode() == BULB_MODE_COLOR) {
applyColor(partialState);
}
break;
case GroupStateField::OH_COLOR:
if (getBulbMode() == BULB_MODE_COLOR) {
applyOhColor(partialState);
}
break;
case GroupStateField::HEX_COLOR:
if (getBulbMode() == BULB_MODE_COLOR) {
applyHexColor(partialState);
}
break;
case GroupStateField::COMPUTED_COLOR:
if (getBulbMode() == BULB_MODE_COLOR) {
applyColor(partialState);
} else {
applyColor(partialState, 255, 255, 255);
}
break;
case GroupStateField::HUE:
if (getBulbMode() == BULB_MODE_COLOR) {
partialState[GroupStateFieldNames::HUE] = getHue();
}
break;
case GroupStateField::SATURATION:
if (getBulbMode() == BULB_MODE_COLOR) {
partialState[GroupStateFieldNames::SATURATION] = getSaturation();
}
break;
case GroupStateField::MODE:
if (getBulbMode() == BULB_MODE_SCENE) {
partialState[GroupStateFieldNames::MODE] = getMode();
}
break;
case GroupStateField::EFFECT:
if (getBulbMode() == BULB_MODE_SCENE) {
partialState[GroupStateFieldNames::EFFECT] = String(getMode());
} else if (isSetBulbMode() && getBulbMode() == BULB_MODE_WHITE) {
partialState[GroupStateFieldNames::EFFECT] = "white_mode";
} else if (getBulbMode() == BULB_MODE_NIGHT) {
partialState[GroupStateFieldNames::EFFECT] = MiLightCommandNames::NIGHT_MODE;
}
break;
case GroupStateField::COLOR_TEMP:
if (isSetBulbMode() && getBulbMode() == BULB_MODE_WHITE) {
partialState[GroupStateFieldNames::COLOR_TEMP] = getMireds();
}
break;
case GroupStateField::KELVIN:
if (isSetBulbMode() && getBulbMode() == BULB_MODE_WHITE) {
partialState[GroupStateFieldNames::KELVIN] = getKelvin();
}
break;
case GroupStateField::DEVICE_ID:
partialState[GroupStateFieldNames::DEVICE_ID] = bulbId.deviceId;
break;
case GroupStateField::GROUP_ID:
partialState[GroupStateFieldNames::GROUP_ID] = bulbId.groupId;
break;
case GroupStateField::DEVICE_TYPE:
{
const MiLightRemoteConfig* remoteConfig = MiLightRemoteConfig::fromType(bulbId.deviceType);
if (remoteConfig) {
partialState[GroupStateFieldNames::DEVICE_TYPE] = remoteConfig->name;
}
}
break;
default:
Serial.printf_P(PSTR("Tried to apply unknown field: %d\n"), static_cast<uint8_t>(field));
break;
}
}
}
// helper function to debug the current state (in JSON) to the serial port
void GroupState::debugState(char const *debugMessage) const {
#ifdef STATE_DEBUG
// using static to keep large buffers off the call stack
StaticJsonDocument<500> jsonDoc;
JsonObject jsonState = jsonDoc.to<JsonObject>();
// define fields to show (if count changes, make sure to update count to applyState below)
std::vector<GroupStateField> fields({
GroupStateField::LEVEL,
GroupStateField::BULB_MODE,
GroupStateField::COLOR_TEMP,
GroupStateField::EFFECT,
GroupStateField::HUE,
GroupStateField::KELVIN,
GroupStateField::MODE,
GroupStateField::SATURATION,
GroupStateField::STATE
});
// Fake id
BulbId id;
// use applyState to build JSON of all fields (from above)
applyState(jsonState, id, fields);
// convert to string and print
Serial.printf("%s: ", debugMessage);
serializeJson(jsonState, Serial);
Serial.println("");
Serial.printf("Raw data: %08X %08X\n", state.rawData[0], state.rawData[1]);
#endif
}
bool GroupState::isSetColor() const {
return isSetHue();
}
ParsedColor GroupState::getColor() const {
uint8_t rgb[3];
RGBConverter converter;
uint16_t hue = getHue();
uint8_t sat = isSetSaturation() ? getSaturation() : 100;
converter.hsvToRgb(
hue / 360.0,
// Default to fully saturated
sat / 100.0,
1,
rgb
);
return {
.success = true,
.hue = hue,
.r = rgb[0],
.g = rgb[1],
.b = rgb[2],
.saturation = sat
};
}
// build up a partial state representation based on the specified GrouipStateField array. Used
// to gather a subset of states (configurable in the UI) for sending to MQTT and web responses.
void GroupState::applyState(JsonObject partialState, const BulbId& bulbId, std::vector<GroupStateField>& fields) const {
for (std::vector<GroupStateField>::const_iterator itr = fields.begin(); itr != fields.end(); ++itr) {
applyField(partialState, bulbId, *itr);
}
}
bool GroupState::isPhysicalField(GroupStateField field) {
for (size_t i = 0; i < size(ALL_PHYSICAL_FIELDS); ++i) {
if (field == ALL_PHYSICAL_FIELDS[i]) {
return true;
}
}
return false;
}