#include #include #include #include #include #include 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(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(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(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(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(field)); break; } return 0; } void GroupState::setFieldValue(GroupStateField field, uint16_t value) { switch (field) { case GroupStateField::STATE: case GroupStateField::STATUS: setState(static_cast(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(value)); break; default: Serial.print(F("WARNING: tried to set value for unknown field: ")); Serial.println(static_cast(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(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(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(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(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(&state.rawData[i]), 4); } clearDirty(); } void GroupState::dump(Stream& stream) const { for (size_t i = 0; i < DATA_LONGS; i++) { stream.write(reinterpret_cast(&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(field)); return false; } int8_t dirValue = static_cast(dir); // If there's already a known value, update it if (previousState != NULL && previousState->isSetField(field)) { int8_t currentValue = static_cast(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(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(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(), 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(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(); // define fields to show (if count changes, make sure to update count to applyState below) std::vector 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& fields) const { for (std::vector::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; }