#include "light_json_schema.h" #include "color_mode.h" #include "light_output.h" #include "esphome/core/progmem.h" #ifdef USE_JSON namespace esphome::light { // See https://www.home-assistant.io/integrations/light.mqtt/#json-schema for documentation on the schema // Color mode JSON strings - packed into flash with compile-time generated offsets. // Indexed by ColorModeBitPolicy bit index (1-9), so index 0 maps to bit 1 ("onoff"). PROGMEM_STRING_TABLE(ColorModeStrings, "onoff", "brightness", "white", "color_temp", "cwww", "rgb", "rgbw", "rgbct", "rgbww"); // Get JSON string for color mode. Returns nullptr for UNKNOWN (bit 0). // Returns ProgmemStr so ArduinoJson knows to handle PROGMEM strings on ESP8266. static ProgmemStr get_color_mode_json_str(ColorMode mode) { unsigned bit = ColorModeBitPolicy::to_bit(mode); if (bit == 0) return nullptr; // bit is 1-9 for valid modes, so bit-1 is always valid (0-8). LAST_INDEX fallback never used. return ColorModeStrings::get_progmem_str(bit - 1, ColorModeStrings::LAST_INDEX); } void LightJSONSchema::dump_json(LightState &state, JsonObject root) { // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson if (state.supports_effects()) { root[ESPHOME_F("effect")] = state.get_effect_name().c_str(); root[ESPHOME_F("effect_index")] = state.get_current_effect_index(); root[ESPHOME_F("effect_count")] = state.get_effect_count(); } auto values = state.remote_values; const auto color_mode = values.get_color_mode(); const auto *mode_str = get_color_mode_json_str(color_mode); if (mode_str != nullptr) { root[ESPHOME_F("color_mode")] = mode_str; } if (color_mode & ColorCapability::ON_OFF) root[ESPHOME_F("state")] = (values.get_state() != 0.0f) ? "ON" : "OFF"; if (color_mode & ColorCapability::BRIGHTNESS) root[ESPHOME_F("brightness")] = to_uint8_scale(values.get_brightness()); JsonObject color = root[ESPHOME_F("color")].to(); if (color_mode & ColorCapability::RGB) { float color_brightness = values.get_color_brightness(); color[ESPHOME_F("r")] = to_uint8_scale(color_brightness * values.get_red()); color[ESPHOME_F("g")] = to_uint8_scale(color_brightness * values.get_green()); color[ESPHOME_F("b")] = to_uint8_scale(color_brightness * values.get_blue()); } if (color_mode & ColorCapability::WHITE) { uint8_t white_val = to_uint8_scale(values.get_white()); color[ESPHOME_F("w")] = white_val; root[ESPHOME_F("white_value")] = white_val; // legacy API } if (color_mode & ColorCapability::COLOR_TEMPERATURE) { // this one isn't under the color subkey for some reason root[ESPHOME_F("color_temp")] = uint32_t(values.get_color_temperature()); } if (color_mode & ColorCapability::COLD_WARM_WHITE) { color[ESPHOME_F("c")] = to_uint8_scale(values.get_cold_white()); color[ESPHOME_F("w")] = to_uint8_scale(values.get_warm_white()); } } void LightJSONSchema::parse_color_json(LightState &state, LightCall &call, JsonObject root) { if (root[ESPHOME_F("state")].is()) { auto val = parse_on_off(root[ESPHOME_F("state")]); switch (val) { case PARSE_ON: call.set_state(true); break; case PARSE_OFF: call.set_state(false); break; case PARSE_TOGGLE: call.set_state(!state.remote_values.is_on()); break; case PARSE_NONE: break; } } if (root[ESPHOME_F("brightness")].is()) { call.set_brightness(float(root[ESPHOME_F("brightness")]) / 255.0f); } if (root[ESPHOME_F("color")].is()) { JsonObject color = root[ESPHOME_F("color")]; // HA also encodes brightness information in the r, g, b values, so extract that and set it as color brightness. float max_rgb = 0.0f; if (color[ESPHOME_F("r")].is()) { float r = float(color[ESPHOME_F("r")]) / 255.0f; max_rgb = fmaxf(max_rgb, r); call.set_red(r); } if (color[ESPHOME_F("g")].is()) { float g = float(color[ESPHOME_F("g")]) / 255.0f; max_rgb = fmaxf(max_rgb, g); call.set_green(g); } if (color[ESPHOME_F("b")].is()) { float b = float(color[ESPHOME_F("b")]) / 255.0f; max_rgb = fmaxf(max_rgb, b); call.set_blue(b); } if (color[ESPHOME_F("r")].is() || color[ESPHOME_F("g")].is() || color[ESPHOME_F("b")].is()) { call.set_color_brightness(max_rgb); } if (color[ESPHOME_F("c")].is()) { call.set_cold_white(float(color[ESPHOME_F("c")]) / 255.0f); } if (color[ESPHOME_F("w")].is()) { // the HA scheme is ambiguous here, the same key is used for white channel in RGBW and warm // white channel in RGBWW. if (color[ESPHOME_F("c")].is()) { call.set_warm_white(float(color[ESPHOME_F("w")]) / 255.0f); } else { call.set_white(float(color[ESPHOME_F("w")]) / 255.0f); } } } if (root[ESPHOME_F("white_value")].is()) { // legacy API call.set_white(float(root[ESPHOME_F("white_value")]) / 255.0f); } if (root[ESPHOME_F("color_temp")].is()) { call.set_color_temperature(float(root[ESPHOME_F("color_temp")])); } } void LightJSONSchema::parse_json(LightState &state, LightCall &call, JsonObject root) { LightJSONSchema::parse_color_json(state, call, root); if (root[ESPHOME_F("flash")].is()) { auto length = uint32_t(float(root[ESPHOME_F("flash")]) * 1000); call.set_flash_length(length); } if (root[ESPHOME_F("transition")].is()) { auto length = uint32_t(float(root[ESPHOME_F("transition")]) * 1000); call.set_transition_length(length); } if (root[ESPHOME_F("effect")].is()) { const char *effect = root[ESPHOME_F("effect")]; call.set_effect(effect, strlen(effect)); } if (root[ESPHOME_F("effect_index")].is()) { uint32_t effect_index = root[ESPHOME_F("effect_index")]; call.set_effect(effect_index); } } } // namespace esphome::light #endif