Added lamp
This commit is contained in:
@@ -0,0 +1,331 @@
|
||||
import esphome.codegen as cg
|
||||
import esphome.config_validation as cv
|
||||
from esphome.components import light
|
||||
from esphome import automation
|
||||
from esphome.core import Lambda
|
||||
from esphome.const import (
|
||||
CONF_RED,
|
||||
CONF_GREEN,
|
||||
CONF_BLUE,
|
||||
CONF_COLOR_TEMPERATURE,
|
||||
CONF_STATE,
|
||||
CONF_OUTPUT_ID,
|
||||
CONF_TRIGGER_ID,
|
||||
CONF_ID,
|
||||
CONF_TRANSITION_LENGTH,
|
||||
CONF_BRIGHTNESS,
|
||||
CONF_EFFECT,
|
||||
CONF_FLASH_LENGTH,
|
||||
CONF_RESTORE_MODE,
|
||||
)
|
||||
from .. import bslamp2_ns, CODEOWNERS, CONF_LIGHT_HAL_ID, LightHAL
|
||||
|
||||
__all__ = ["CODEOWNERS"]
|
||||
|
||||
DEPENDENCIES = ["xiaomi_bslamp2"]
|
||||
|
||||
CONF_ON_BRIGHTNESS = "on_brightness"
|
||||
CONF_PRESET_ID = "preset_id"
|
||||
CONF_PRESETS_ID = "presets_id"
|
||||
CONF_PRESET = "preset"
|
||||
CONF_PRESETS = "presets"
|
||||
CONF_NEXT = "next"
|
||||
CONF_GROUP = "group"
|
||||
CONF_NIGHT_CALIBRATION = "night_calibration"
|
||||
|
||||
MIRED_MIN = 153
|
||||
MIRED_MAX = 588
|
||||
DEFAULT_NIGHT_LIGHT_RED = 0.968
|
||||
DEFAULT_NIGHT_LIGHT_GREEN = 0.968
|
||||
DEFAULT_NIGHT_LIGHT_BLUE = 0.972
|
||||
|
||||
XiaomiBslamp2LightState = bslamp2_ns.class_("XiaomiBslamp2LightState", light.LightState)
|
||||
XiaomiBslamp2LightOutput = bslamp2_ns.class_("XiaomiBslamp2LightOutput", light.LightOutput)
|
||||
PresetsContainer = bslamp2_ns.class_("PresetsContainer", cg.Component)
|
||||
Preset = bslamp2_ns.class_("Preset", cg.Component)
|
||||
BrightnessTrigger = bslamp2_ns.class_("BrightnessTrigger", automation.Trigger.template())
|
||||
ActivatePresetAction = bslamp2_ns.class_("ActivatePresetAction", automation.Action)
|
||||
DiscoAction = bslamp2_ns.class_("DiscoAction", automation.Action)
|
||||
|
||||
PRESETS_SCHEMA = cv.Schema({str.lower: cv.Schema({str.lower: light.automation.LIGHT_TURN_ON_ACTION_SCHEMA})})
|
||||
|
||||
|
||||
def validate_preset(config):
|
||||
has_rgb = CONF_RED in config or CONF_GREEN in config or CONF_BLUE in config
|
||||
has_white = CONF_COLOR_TEMPERATURE in config
|
||||
has_effect = CONF_EFFECT in config
|
||||
|
||||
# Check mutual exclusivity of preset options.
|
||||
if (has_rgb + has_white + has_effect) > 1:
|
||||
raise cv.Invalid("Use only one of RGB light, white (color temperature) light or an effect")
|
||||
|
||||
# Check the color temperature value range.
|
||||
if has_white:
|
||||
if config[CONF_COLOR_TEMPERATURE] < MIRED_MIN or config[CONF_COLOR_TEMPERATURE] > MIRED_MAX:
|
||||
raise cv.Invalid(f"The color temperature must be in the range {MIRED_MIN} - {MIRED_MAX}")
|
||||
|
||||
# When defining an RGB color, it is allowed to omit RGB components that have value 0.
|
||||
if has_rgb:
|
||||
if CONF_RED not in config:
|
||||
config[CONF_RED] = 0
|
||||
if CONF_GREEN not in config:
|
||||
config[CONF_GREEN] = 0
|
||||
if CONF_BLUE not in config:
|
||||
config[CONF_BLUE] = 0
|
||||
|
||||
return config
|
||||
|
||||
|
||||
PRESET_SCHEMA = cv.All(
|
||||
cv.Schema(
|
||||
{
|
||||
cv.GenerateID(CONF_ID): cv.use_id(XiaomiBslamp2LightState),
|
||||
cv.GenerateID(CONF_PRESET_ID): cv.declare_id(Preset),
|
||||
cv.Optional(CONF_EFFECT): cv.string,
|
||||
cv.Optional(CONF_COLOR_TEMPERATURE): cv.color_temperature,
|
||||
cv.Optional(CONF_RED): cv.percentage,
|
||||
cv.Optional(CONF_GREEN): cv.percentage,
|
||||
cv.Optional(CONF_BLUE): cv.percentage,
|
||||
cv.Optional(CONF_BRIGHTNESS): cv.percentage,
|
||||
cv.Optional(CONF_TRANSITION_LENGTH): cv.positive_time_period_milliseconds,
|
||||
}
|
||||
),
|
||||
validate_preset,
|
||||
)
|
||||
|
||||
CONFIG_SCHEMA = light.RGB_LIGHT_SCHEMA.extend(
|
||||
{
|
||||
cv.GenerateID(CONF_ID): cv.declare_id(XiaomiBslamp2LightState),
|
||||
cv.GenerateID(CONF_LIGHT_HAL_ID): cv.use_id(LightHAL),
|
||||
cv.GenerateID(CONF_OUTPUT_ID): cv.declare_id(XiaomiBslamp2LightOutput),
|
||||
cv.Optional(CONF_RESTORE_MODE, default="RESTORE_DEFAULT_OFF"): cv.enum(
|
||||
light.RESTORE_MODES, upper=True, space="_"
|
||||
),
|
||||
cv.Optional(CONF_NIGHT_CALIBRATION, default={}): cv.Schema(
|
||||
{
|
||||
cv.Optional(CONF_RED, default=DEFAULT_NIGHT_LIGHT_RED): cv.float_range(min=0.0, max=1.0),
|
||||
cv.Optional(CONF_GREEN, default=DEFAULT_NIGHT_LIGHT_GREEN): cv.float_range(min=0.0, max=1.0),
|
||||
cv.Optional(CONF_BLUE, default=DEFAULT_NIGHT_LIGHT_BLUE): cv.float_range(min=0.0, max=1.0),
|
||||
}
|
||||
),
|
||||
cv.Optional(CONF_ON_BRIGHTNESS): automation.validate_automation(
|
||||
{
|
||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(BrightnessTrigger),
|
||||
}
|
||||
),
|
||||
cv.GenerateID(CONF_PRESETS_ID): cv.declare_id(PresetsContainer),
|
||||
cv.Optional(CONF_PRESETS): cv.Schema({str.lower: cv.Schema({str.lower: PRESET_SCHEMA})}),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def is_preset_group(value):
|
||||
return value
|
||||
|
||||
|
||||
def is_preset(value):
|
||||
return value
|
||||
|
||||
|
||||
def maybe_simple_preset_action(schema):
|
||||
def validator(value):
|
||||
if isinstance(value, dict):
|
||||
return schema(value)
|
||||
value = value.lower()
|
||||
config = {}
|
||||
if value == "next_group":
|
||||
config[CONF_NEXT] = CONF_GROUP
|
||||
elif value == "next_preset":
|
||||
config[CONF_NEXT] = CONF_PRESET
|
||||
elif "." not in value:
|
||||
config[CONF_GROUP] = value
|
||||
else:
|
||||
group, preset = value.split(".", 2)
|
||||
config[CONF_GROUP] = group
|
||||
config[CONF_PRESET] = preset
|
||||
return schema(config)
|
||||
|
||||
return validator
|
||||
|
||||
|
||||
@automation.register_action(
|
||||
"light.disco_on",
|
||||
DiscoAction,
|
||||
light.automation.LIGHT_TURN_ON_ACTION_SCHEMA,
|
||||
synchronous=True,
|
||||
)
|
||||
def disco_action_on_to_code(config, action_id, template_arg, args):
|
||||
light_var = yield cg.get_variable(config[CONF_ID])
|
||||
var = cg.new_Pvariable(action_id, template_arg, light_var)
|
||||
|
||||
if CONF_STATE in config:
|
||||
template_ = yield cg.templatable(config[CONF_STATE], args, bool)
|
||||
cg.add(var.set_state(template_))
|
||||
if CONF_TRANSITION_LENGTH in config:
|
||||
template_ = yield cg.templatable(config[CONF_TRANSITION_LENGTH], args, cg.uint32)
|
||||
cg.add(var.set_transition_length(template_))
|
||||
if CONF_FLASH_LENGTH in config:
|
||||
template_ = yield cg.templatable(config[CONF_FLASH_LENGTH], args, cg.uint32)
|
||||
cg.add(var.set_flash_length(template_))
|
||||
if CONF_BRIGHTNESS in config:
|
||||
template_ = yield cg.templatable(config[CONF_BRIGHTNESS], args, float)
|
||||
cg.add(var.set_brightness(template_))
|
||||
if CONF_RED in config:
|
||||
template_ = yield cg.templatable(config[CONF_RED], args, float)
|
||||
cg.add(var.set_red(template_))
|
||||
if CONF_GREEN in config:
|
||||
template_ = yield cg.templatable(config[CONF_GREEN], args, float)
|
||||
cg.add(var.set_green(template_))
|
||||
if CONF_BLUE in config:
|
||||
template_ = yield cg.templatable(config[CONF_BLUE], args, float)
|
||||
cg.add(var.set_blue(template_))
|
||||
if CONF_COLOR_TEMPERATURE in config:
|
||||
template_ = yield cg.templatable(config[CONF_COLOR_TEMPERATURE], args, float)
|
||||
cg.add(var.set_color_temperature(template_))
|
||||
if CONF_EFFECT in config:
|
||||
template_ = yield cg.templatable(config[CONF_EFFECT], args, cg.std_string)
|
||||
cg.add(var.set_effect(template_))
|
||||
yield var
|
||||
|
||||
|
||||
@automation.register_action(
|
||||
"light.disco_off",
|
||||
DiscoAction,
|
||||
light.automation.LIGHT_TURN_OFF_ACTION_SCHEMA,
|
||||
synchronous=True,
|
||||
)
|
||||
async def disco_action_off_to_code(config, action_id, template_arg, args):
|
||||
light_var = await cg.get_variable(config[CONF_ID])
|
||||
var = cg.new_Pvariable(action_id, template_arg, light_var)
|
||||
template_ = await cg.templatable(False, args, bool)
|
||||
cg.add(var.set_disco_state(template_))
|
||||
return var
|
||||
|
||||
|
||||
USED_PRESETS = []
|
||||
|
||||
|
||||
def register_preset_action(value):
|
||||
if "group" in value and not isinstance(value["group"], Lambda):
|
||||
if "preset" in value and not isinstance(value["preset"], Lambda):
|
||||
preset_data = [value["group"], value["preset"]]
|
||||
else:
|
||||
preset_data = [value["group"], None]
|
||||
USED_PRESETS.append(preset_data)
|
||||
return value
|
||||
|
||||
|
||||
@automation.register_action(
|
||||
"preset.activate",
|
||||
ActivatePresetAction,
|
||||
cv.All(
|
||||
maybe_simple_preset_action(
|
||||
cv.Any(
|
||||
cv.Schema(
|
||||
{
|
||||
cv.GenerateID(CONF_PRESETS_ID): cv.use_id(PresetsContainer),
|
||||
cv.Required(CONF_GROUP): cv.templatable(cv.string),
|
||||
cv.Optional(CONF_PRESET): cv.templatable(cv.string),
|
||||
}
|
||||
),
|
||||
cv.Schema(
|
||||
{
|
||||
cv.GenerateID(CONF_PRESETS_ID): cv.use_id(PresetsContainer),
|
||||
cv.Required(CONF_NEXT): cv.one_of(CONF_GROUP, CONF_PRESET, lower=True),
|
||||
}
|
||||
),
|
||||
)
|
||||
),
|
||||
register_preset_action,
|
||||
),
|
||||
synchronous=True,
|
||||
)
|
||||
def preset_activate_to_code(config, action_id, template_arg, args):
|
||||
presets_var = yield cg.get_variable(config[CONF_PRESETS_ID])
|
||||
action_var = cg.new_Pvariable(action_id, template_arg, presets_var)
|
||||
if CONF_NEXT in config:
|
||||
cg.add(action_var.set_operation(f"next_{config[CONF_NEXT]}"))
|
||||
elif CONF_PRESET in config:
|
||||
cg.add(action_var.set_operation("activate_preset"))
|
||||
group_template_ = yield cg.templatable(config[CONF_GROUP], args, cg.std_string)
|
||||
cg.add(action_var.set_group(group_template_))
|
||||
preset_template_ = yield cg.templatable(config[CONF_PRESET], args, cg.std_string)
|
||||
cg.add(action_var.set_preset(preset_template_))
|
||||
else:
|
||||
cg.add(action_var.set_operation("activate_group"))
|
||||
group_template_ = yield cg.templatable(config[CONF_GROUP], args, cg.std_string)
|
||||
cg.add(action_var.set_group(group_template_))
|
||||
yield action_var
|
||||
|
||||
|
||||
async def light_output_to_code(config):
|
||||
light_output_var = cg.new_Pvariable(config[CONF_OUTPUT_ID])
|
||||
await light.register_light(light_output_var, config)
|
||||
light_hal_var = await cg.get_variable(config[CONF_LIGHT_HAL_ID])
|
||||
cg.add(light_output_var.set_parent(light_hal_var))
|
||||
night_calibration = config[CONF_NIGHT_CALIBRATION]
|
||||
cg.add(
|
||||
light_output_var.set_night_light_color_temperature_calibration(
|
||||
night_calibration[CONF_RED],
|
||||
night_calibration[CONF_GREEN],
|
||||
night_calibration[CONF_BLUE],
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
async def on_brightness_to_code(config):
|
||||
light_output_var = await cg.get_variable(config[CONF_OUTPUT_ID])
|
||||
for config_ in config.get(CONF_ON_BRIGHTNESS, []):
|
||||
trigger = cg.new_Pvariable(config_[CONF_TRIGGER_ID], light_output_var)
|
||||
await automation.build_automation(trigger, [(float, "x")], config_)
|
||||
|
||||
|
||||
async def preset_to_code(config, preset_group, preset_name):
|
||||
light_var = await cg.get_variable(config[CONF_ID])
|
||||
preset_var = cg.new_Pvariable(config[CONF_PRESET_ID], light_var, preset_group, preset_name)
|
||||
if CONF_TRANSITION_LENGTH in config:
|
||||
cg.add(preset_var.set_transition_length(config[CONF_TRANSITION_LENGTH]))
|
||||
if CONF_BRIGHTNESS in config:
|
||||
cg.add(preset_var.set_brightness(config[CONF_BRIGHTNESS]))
|
||||
if CONF_RED in config:
|
||||
cg.add(preset_var.set_red(config[CONF_RED]))
|
||||
if CONF_GREEN in config:
|
||||
cg.add(preset_var.set_green(config[CONF_GREEN]))
|
||||
if CONF_BLUE in config:
|
||||
cg.add(preset_var.set_blue(config[CONF_BLUE]))
|
||||
if CONF_COLOR_TEMPERATURE in config:
|
||||
cg.add(preset_var.set_color_temperature(config[CONF_COLOR_TEMPERATURE]))
|
||||
if CONF_EFFECT in config:
|
||||
cg.add(preset_var.set_effect(config[CONF_EFFECT]))
|
||||
else:
|
||||
cg.add(preset_var.set_effect("None"))
|
||||
return await cg.register_component(preset_var, config)
|
||||
|
||||
|
||||
async def presets_to_code(config):
|
||||
presets_var = cg.new_Pvariable(config[CONF_PRESETS_ID])
|
||||
await cg.register_component(presets_var, config)
|
||||
|
||||
for preset_group, presets in config.get(CONF_PRESETS, {}).items():
|
||||
for preset_name, preset_config in presets.items():
|
||||
preset = await preset_to_code(preset_config, preset_group, preset_name)
|
||||
cg.add(presets_var.add_preset(preset))
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
await light_output_to_code(config)
|
||||
await on_brightness_to_code(config)
|
||||
await presets_to_code(config)
|
||||
|
||||
|
||||
def validate(config):
|
||||
valid_presets = config.get(CONF_PRESETS, {})
|
||||
for group, preset in USED_PRESETS:
|
||||
if group not in valid_presets:
|
||||
raise cv.Invalid(f"Invalid light preset group '{group}' used")
|
||||
if preset is not None and preset not in valid_presets[group]:
|
||||
raise cv.Invalid(f"Invalid light preset '{group}.{preset}' used")
|
||||
return config
|
||||
|
||||
|
||||
FINAL_VALIDATE_SCHEMA = cv.Schema(validate)
|
||||
@@ -0,0 +1,109 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/core/automation.h"
|
||||
#include "esphome/core/component.h"
|
||||
#include "interfaces.h"
|
||||
#include "light_output.h"
|
||||
#include "light_state.h"
|
||||
#include "presets.h"
|
||||
#include <cmath>
|
||||
|
||||
namespace esphome {
|
||||
namespace xiaomi {
|
||||
namespace bslamp2 {
|
||||
|
||||
class BrightnessTrigger : public Trigger<float> {
|
||||
public:
|
||||
explicit BrightnessTrigger(XiaomiBslamp2LightOutput *parent) {
|
||||
parent->add_on_state_callback([this](light::LightColorValues values) {
|
||||
auto new_brightness = values.get_brightness();
|
||||
if (values.get_state() == 0)
|
||||
new_brightness = 0.0f;
|
||||
new_brightness = roundf(new_brightness * 100.0f) / 100.0f;
|
||||
if (last_brightness_ != new_brightness) {
|
||||
trigger(new_brightness);
|
||||
last_brightness_ = new_brightness;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected:
|
||||
float last_brightness_ = -1.0f;
|
||||
};
|
||||
|
||||
template<typename... Ts> class DiscoAction : public Action<Ts...> {
|
||||
public:
|
||||
explicit DiscoAction(LightStateDiscoSupport *parent) : parent_(parent) {}
|
||||
|
||||
TEMPLATABLE_VALUE(bool, disco_state)
|
||||
TEMPLATABLE_VALUE(bool, state)
|
||||
TEMPLATABLE_VALUE(uint32_t, transition_length)
|
||||
TEMPLATABLE_VALUE(uint32_t, flash_length)
|
||||
TEMPLATABLE_VALUE(float, brightness)
|
||||
TEMPLATABLE_VALUE(float, red)
|
||||
TEMPLATABLE_VALUE(float, green)
|
||||
TEMPLATABLE_VALUE(float, blue)
|
||||
TEMPLATABLE_VALUE(float, color_temperature)
|
||||
TEMPLATABLE_VALUE(std::string, effect)
|
||||
|
||||
void play(const Ts &...x) override {
|
||||
if (this->disco_state_.has_value()) {
|
||||
auto p = this->disco_state_.optional_value(x...);
|
||||
if (!*p) {
|
||||
parent_->disco_stop();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
auto call = this->parent_->make_disco_call(false);
|
||||
call.set_state(this->state_.optional_value(x...));
|
||||
call.set_brightness(this->brightness_.optional_value(x...));
|
||||
call.set_red(this->red_.optional_value(x...));
|
||||
call.set_green(this->green_.optional_value(x...));
|
||||
call.set_blue(this->blue_.optional_value(x...));
|
||||
call.set_color_temperature(this->color_temperature_.optional_value(x...));
|
||||
call.set_effect(this->effect_.optional_value(x...));
|
||||
call.set_flash_length(this->flash_length_.optional_value(x...));
|
||||
call.set_transition_length(this->transition_length_.optional_value(x...));
|
||||
|
||||
// Force the light to update right now, not in the next loop.
|
||||
call.perform();
|
||||
parent_->disco_apply();
|
||||
}
|
||||
|
||||
protected:
|
||||
LightStateDiscoSupport *parent_;
|
||||
};
|
||||
|
||||
template<typename... Ts> class ActivatePresetAction : public Action<Ts...> {
|
||||
public:
|
||||
explicit ActivatePresetAction(PresetsContainer *presets) : presets_(presets) {}
|
||||
|
||||
TEMPLATABLE_VALUE(std::string, operation);
|
||||
TEMPLATABLE_VALUE(std::string, group);
|
||||
TEMPLATABLE_VALUE(std::string, preset);
|
||||
|
||||
void play(const Ts &...x) override {
|
||||
auto operation = this->operation_.value(x...);
|
||||
|
||||
if (operation == "next_group") {
|
||||
presets_->activate_next_group();
|
||||
} else if (operation == "next_preset") {
|
||||
presets_->activate_next_preset();
|
||||
} else if (operation == "activate_group") {
|
||||
auto group = this->group_.value(x...);
|
||||
presets_->activate_group(group);
|
||||
} else if (operation == "activate_preset") {
|
||||
auto group = this->group_.value(x...);
|
||||
auto preset = this->preset_.value(x...);
|
||||
presets_->activate_preset(group, preset);
|
||||
}
|
||||
}
|
||||
|
||||
protected:
|
||||
PresetsContainer *presets_;
|
||||
};
|
||||
|
||||
} // namespace bslamp2
|
||||
} // namespace xiaomi
|
||||
} // namespace esphome
|
||||
@@ -0,0 +1,28 @@
|
||||
#pragma once
|
||||
|
||||
#include "../light_hal.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace xiaomi {
|
||||
namespace bslamp2 {
|
||||
|
||||
/**
|
||||
* This abstract class is used for implementing classes that translate
|
||||
* LightColorValues into the required GPIO PWM duty cycle levels to represent
|
||||
* the requested color on the physical device.
|
||||
*/
|
||||
class ColorHandler : public GPIOOutputValues {
|
||||
public:
|
||||
/**
|
||||
* Sets the red, green, blue, white fields to the PWM duty cycles
|
||||
* that are required to represent the requested light color for
|
||||
* the provided LightColorValues input.
|
||||
*
|
||||
* Returns true when the input can be handled, false otherwise.
|
||||
*/
|
||||
virtual bool set_light_color_values(light::LightColorValues v) = 0;
|
||||
};
|
||||
|
||||
} // namespace bslamp2
|
||||
} // namespace xiaomi
|
||||
} // namespace esphome
|
||||
@@ -0,0 +1,68 @@
|
||||
#pragma once
|
||||
|
||||
#include <array>
|
||||
#include <stdexcept>
|
||||
|
||||
#include "../common.h"
|
||||
#include "color_handler.h"
|
||||
#include "color_handler_off.h"
|
||||
#include "color_handler_rgb.h"
|
||||
#include "color_handler_color_temperature.h"
|
||||
#include "color_handler_night_light.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace xiaomi {
|
||||
namespace bslamp2 {
|
||||
|
||||
/**
|
||||
* This class translates LightColorValues into GPIO duty cycles that can be
|
||||
* used for representing a requested light color on the physical device.
|
||||
*
|
||||
* The code handles all known light modes for the device:
|
||||
*
|
||||
* - off: the light is off
|
||||
* - night light: based on RGB or white mode + lowest possible brightness
|
||||
* - white light: based on color temperature + brightness
|
||||
* - RGB light: based on RGB values + brightness
|
||||
*/
|
||||
class ColorHandlerChain : public ColorHandler {
|
||||
public:
|
||||
void set_night_light_color_temperature_calibration(NightLightCalibration calibration) {
|
||||
night_light_.set_color_temperature_calibration(calibration);
|
||||
}
|
||||
|
||||
void set_night_light_color_temperature_calibration(float red, float green, float blue) {
|
||||
set_night_light_color_temperature_calibration({red, green, blue});
|
||||
}
|
||||
|
||||
bool set_light_color_values(light::LightColorValues v) {
|
||||
// The actual implementation of the various light modes is in separate
|
||||
// targeted classes. These classes are called here in a chain of
|
||||
// command-like pattern, to let the first one that can handle the light
|
||||
// settings do the honours.
|
||||
if (off_light_.set_light_color_values(v))
|
||||
off_light_.copy_to(this);
|
||||
else if (night_light_.set_light_color_values(v))
|
||||
night_light_.copy_to(this);
|
||||
else if (white_light_.set_light_color_values(v))
|
||||
white_light_.copy_to(this);
|
||||
else if (rgb_light_.set_light_color_values(v))
|
||||
rgb_light_.copy_to(this);
|
||||
else {
|
||||
ESP_LOGE(TAG, "Light color error: (None of the ColorHandler classes handles the requested light state; defaulting to 'off'");
|
||||
off_light_.copy_to(this);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
protected:
|
||||
ColorHandlerOff off_light_;
|
||||
ColorHandlerRGB rgb_light_;
|
||||
ColorHandlerColorTemperature white_light_;
|
||||
ColorHandlerNightLight night_light_;
|
||||
};
|
||||
|
||||
} // namespace bslamp2
|
||||
} // namespace xiaomi
|
||||
} // namespace esphome
|
||||
@@ -0,0 +1,131 @@
|
||||
#pragma once
|
||||
|
||||
#include <array>
|
||||
#include <stdexcept>
|
||||
|
||||
#include "../common.h"
|
||||
#include "../light_hal.h"
|
||||
#include "color_handler.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace xiaomi {
|
||||
namespace bslamp2 {
|
||||
|
||||
/**
|
||||
* The minimum color temperature in mired.
|
||||
* Same as supported by the original firmware.
|
||||
*/
|
||||
static const int MIRED_MIN = 153;
|
||||
|
||||
/**
|
||||
* The maximum color temperature in mired.
|
||||
* Same as supported by the original firmware.
|
||||
*/
|
||||
static const int MIRED_MAX = 588;
|
||||
|
||||
struct RGBWLevelsByTemperature {
|
||||
float from_temperature;
|
||||
float red;
|
||||
float green;
|
||||
float blue;
|
||||
float white;
|
||||
};
|
||||
|
||||
using RGBWLevelsTable = std::array<RGBWLevelsByTemperature, 15>;
|
||||
|
||||
// clang-format off
|
||||
static const RGBWLevelsTable rgbw_levels_1_ {{
|
||||
{ 501.0f, 0.873f, 0.907f, 1.000f, 0.063f },
|
||||
{ 455.0f, 0.873f, 0.896f, 1.000f, 0.063f },
|
||||
{ 417.0f, 0.873f, 0.891f, 1.000f, 0.068f },
|
||||
{ 371.0f, 0.873f, 0.880f, 1.000f, 0.070f },
|
||||
{ 334.0f, 0.873f, 0.887f, 1.000f, 0.088f },
|
||||
{ 313.0f, 0.882f, 0.904f, 1.000f, 0.128f },
|
||||
{ 295.0f, 0.947f, 1.000f, 0.968f, 0.145f },
|
||||
{ 251.0f, 0.999f, 1.000f, 1.000f, 0.155f },
|
||||
{ 223.0f, 1.000f, 0.899f, 0.921f, 0.130f },
|
||||
{ 201.0f, 1.000f, 0.873f, 0.908f, 0.115f },
|
||||
{ 182.0f, 1.000f, 0.873f, 0.901f, 0.103f },
|
||||
{ 173.0f, 1.000f, 0.873f, 0.904f, 0.094f },
|
||||
{ 167.0f, 1.000f, 0.873f, 0.891f, 0.098f },
|
||||
{ 154.0f, 1.000f, 0.873f, 0.894f, 0.090f },
|
||||
{ 153.0f, 1.000f, 0.873f, 0.892f, 0.088f }
|
||||
}};
|
||||
|
||||
static const RGBWLevelsTable rgbw_levels_100_ {{
|
||||
{ 501.0f, 0.000f, 0.344f, 1.000f, 0.068f },
|
||||
{ 455.0f, 0.000f, 0.237f, 1.000f, 0.093f },
|
||||
{ 417.0f, 0.000f, 0.186f, 1.000f, 0.120f },
|
||||
{ 371.0f, 0.000f, 0.149f, 1.000f, 0.167f },
|
||||
{ 334.0f, 0.000f, 0.135f, 1.000f, 0.325f },
|
||||
{ 313.0f, 0.097f, 0.314f, 1.000f, 0.740f },
|
||||
{ 295.0f, 0.745f, 1.000f, 0.953f, 0.905f },
|
||||
{ 251.0f, 1.000f, 1.000f, 1.000f, 1.000f },
|
||||
{ 223.0f, 1.000f, 0.267f, 0.485f, 0.765f },
|
||||
{ 201.0f, 1.000f, 0.000f, 0.355f, 0.609f },
|
||||
{ 182.0f, 1.000f, 0.000f, 0.282f, 0.489f },
|
||||
{ 173.0f, 1.000f, 0.000f, 0.313f, 0.392f },
|
||||
{ 167.0f, 1.000f, 0.000f, 0.180f, 0.422f },
|
||||
{ 154.0f, 1.000f, 0.000f, 0.218f, 0.368f },
|
||||
{ 153.0f, 1.000f, 0.000f, 0.187f, 0.335f }
|
||||
}};
|
||||
// clang-format on
|
||||
|
||||
/**
|
||||
* This class can handle the GPIO outputs for the white light mode,
|
||||
* based on color temperature + brightness.
|
||||
*/
|
||||
class ColorHandlerColorTemperature : public ColorHandler {
|
||||
public:
|
||||
bool set_light_color_values(light::LightColorValues v) {
|
||||
light_mode = LIGHT_MODE_WHITE;
|
||||
|
||||
if (v.get_color_mode() != light::ColorMode::COLOR_TEMPERATURE) {
|
||||
return false;
|
||||
}
|
||||
|
||||
auto temperature = clamp_temperature_(v.get_color_temperature());
|
||||
auto brightness = clamp_brightness_(v.get_brightness());
|
||||
|
||||
auto levels_1 = lookup_in_table_(rgbw_levels_1_, temperature);
|
||||
auto levels_100 = lookup_in_table_(rgbw_levels_100_, temperature);
|
||||
|
||||
red = std::lerp(levels_1.red, levels_100.red, brightness);
|
||||
green = std::lerp(levels_1.green, levels_100.green, brightness);
|
||||
blue = std::lerp(levels_1.blue, levels_100.blue, brightness);
|
||||
white = std::lerp(levels_1.white, levels_100.white, brightness);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
protected:
|
||||
float clamp_temperature_(float temperature) {
|
||||
if (temperature > MIRED_MAX)
|
||||
temperature = MIRED_MAX;
|
||||
else if (temperature < MIRED_MIN)
|
||||
temperature = MIRED_MIN;
|
||||
return temperature;
|
||||
}
|
||||
|
||||
float clamp_brightness_(float brightness) {
|
||||
if (brightness < 0.01f)
|
||||
brightness = 0.01f;
|
||||
else if (brightness > 1.00f)
|
||||
brightness = 1.00f;
|
||||
return brightness;
|
||||
}
|
||||
|
||||
RGBWLevelsByTemperature lookup_in_table_(RGBWLevelsTable table, float temperature) {
|
||||
for (RGBWLevelsByTemperature& item : table)
|
||||
if (temperature >= item.from_temperature)
|
||||
return item;
|
||||
// Temperature too low. Shouldn't happen, because of validation
|
||||
// at higher levels. But when it happens, simply return the data
|
||||
// for lowest available temperature.
|
||||
return table[0];
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace bslamp2
|
||||
} // namespace xiaomi
|
||||
} // namespace esphome
|
||||
@@ -0,0 +1,85 @@
|
||||
#pragma once
|
||||
|
||||
#include "../common.h"
|
||||
#include "../light_hal.h"
|
||||
#include "color_handler.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace xiaomi {
|
||||
namespace bslamp2 {
|
||||
|
||||
struct NightLightCalibration {
|
||||
float red;
|
||||
float green;
|
||||
float blue;
|
||||
};
|
||||
|
||||
static const NightLightCalibration DEFAULT_NIGHT_LIGHT_CALIBRATION{0.968f, 0.968f, 0.972f};
|
||||
|
||||
/**
|
||||
* This class can handle the GPIO outputs for the night light mode.
|
||||
*
|
||||
* At the lowest brightness setting, the light will switch to night light
|
||||
* mode. In the Yeelight integration in Home Assistant, this feature is
|
||||
* exposed trough a separate switch. I have found that the switch is both
|
||||
* confusing and made me run into issues when automating the lights.
|
||||
* Using the lowest brightness for triggering the night light feels a lot
|
||||
* more natural.
|
||||
*
|
||||
* Note that if a switch is still the preferred way to handle the night
|
||||
* light mode toggle, then this still could be implemented through the
|
||||
* device's yaml configuration.
|
||||
*/
|
||||
class ColorHandlerNightLight : public ColorHandler {
|
||||
public:
|
||||
void set_color_temperature_calibration(NightLightCalibration calibration) {
|
||||
color_temperature_calibration_.red = clamp(calibration.red, 0.0f, 1.0f);
|
||||
color_temperature_calibration_.green = clamp(calibration.green, 0.0f, 1.0f);
|
||||
color_temperature_calibration_.blue = clamp(calibration.blue, 0.0f, 1.0f);
|
||||
}
|
||||
|
||||
void set_color_temperature_calibration(float red, float green, float blue) {
|
||||
set_color_temperature_calibration({red, green, blue});
|
||||
}
|
||||
|
||||
bool set_light_color_values(light::LightColorValues v) {
|
||||
light_mode = LIGHT_MODE_NIGHT;
|
||||
|
||||
// Note: I do not check for a brightness at or below 0.01 (1%) here,
|
||||
// because the lowest brightness setting from Home Assistant turns
|
||||
// up as 0.011765 in here (which is 3/255 and not 1/100).
|
||||
if (v.get_brightness() >= 0.012f)
|
||||
return false;
|
||||
|
||||
// This night light mode is activated when white light is selected.
|
||||
// Based on measurements using the original device firmware, so it
|
||||
// matches the night light of the original firmware.
|
||||
if (v.get_color_mode() == light::ColorMode::COLOR_TEMPERATURE) {
|
||||
red = color_temperature_calibration_.red;
|
||||
green = color_temperature_calibration_.green;
|
||||
blue = color_temperature_calibration_.blue;
|
||||
white = 0.0f;
|
||||
}
|
||||
// In RGB mode, the selected color is used to give the night light a
|
||||
// specific color, instead of the default. This is a nice extra for
|
||||
// this firmware, as the original firmware does not support it.
|
||||
else {
|
||||
red = std::lerp(0.9997f, 0.9680f, v.get_red());
|
||||
green = std::lerp(0.9997f, 0.9680f, v.get_green());
|
||||
auto blue_scale = (v.get_red() + v.get_green()) / 2.0f;
|
||||
auto blue_max = std::lerp(0.9640f, 0.9720f, blue_scale);
|
||||
blue = std::lerp(0.9997f, blue_max, v.get_blue());
|
||||
white = 0.0f;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
protected:
|
||||
NightLightCalibration color_temperature_calibration_{DEFAULT_NIGHT_LIGHT_CALIBRATION};
|
||||
};
|
||||
|
||||
} // namespace bslamp2
|
||||
} // namespace xiaomi
|
||||
} // namespace esphome
|
||||
@@ -0,0 +1,33 @@
|
||||
#pragma once
|
||||
|
||||
#include "../common.h"
|
||||
#include "../light_hal.h"
|
||||
#include "color_handler.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace xiaomi {
|
||||
namespace bslamp2 {
|
||||
|
||||
/**
|
||||
* This class can handle the GPIO outputs in case the light is turned off.
|
||||
*/
|
||||
class ColorHandlerOff : public ColorHandler {
|
||||
public:
|
||||
bool set_light_color_values(light::LightColorValues v) {
|
||||
light_mode = LIGHT_MODE_OFF;
|
||||
|
||||
if (v.get_state() != 0.0f && v.get_brightness() != 0.0f)
|
||||
return false;
|
||||
|
||||
red = 1.0f;
|
||||
green = 1.0f;
|
||||
blue = 1.0f;
|
||||
white = 0.0f;
|
||||
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace bslamp2
|
||||
} // namespace xiaomi
|
||||
} // namespace esphome
|
||||
@@ -0,0 +1,379 @@
|
||||
#pragma once
|
||||
|
||||
#include <array>
|
||||
#include <cmath>
|
||||
#include <algorithm>
|
||||
|
||||
#include "../common.h"
|
||||
#include "../light_hal.h"
|
||||
#include "color_handler.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace xiaomi {
|
||||
namespace bslamp2 {
|
||||
|
||||
struct RGB {
|
||||
float red;
|
||||
float green;
|
||||
float blue;
|
||||
};
|
||||
|
||||
struct RGBPoint {
|
||||
RGB low;
|
||||
RGB high;
|
||||
};
|
||||
|
||||
using RGBRing = std::array<RGBPoint, 24>;
|
||||
|
||||
using RGBCircle = std::array<RGBRing, 7>;
|
||||
|
||||
/**
|
||||
* The following table contains GPIO PWM duty cycles as used for driving
|
||||
* the LEDs in the device in RGB mode.
|
||||
*
|
||||
* The base for this table are measurements against the original device
|
||||
* firmware, using the RGB color circle as used in Home Assistant as the
|
||||
* color space model.
|
||||
*
|
||||
* This circle has 7 colored rings around a white center point. The outer
|
||||
* ring, with the highest saturation, is numbered as 0. The inner ring
|
||||
* around the white center point is numbered as 6. The white center point
|
||||
* itself is numbered as 7, although this one cannot really be called "a
|
||||
* ring".
|
||||
*
|
||||
* For each ring, there are 24 color positions, starting at the color red
|
||||
* (0°), going around the circle clockwise via green (120°) and blue (240°).
|
||||
*
|
||||
* For each color position, two duty cycle measurements are registered:
|
||||
* - one defining the duty cycles at 1% brightness
|
||||
* - one defining the duty cycles at 100% brightness Duty cycles for
|
||||
* in-between brightnesses can be derived from these values by means of
|
||||
* linear interpolation.
|
||||
*/
|
||||
// clang-format off
|
||||
static const RGBCircle rgb_circle_ {{
|
||||
// Ring 0, min value RGB component value = 0
|
||||
{{
|
||||
{{ 0.8998, 0.9997, 0.9997 }, { 0.0000, 0.9997, 0.9997 }}, // 0° [255,0,0] (red)
|
||||
{{ 0.8727, 0.9404, 0.9682 }, { 0.0000, 0.6758, 0.9539 }}, // 15° [255,0,63]
|
||||
{{ 0.8727, 0.8967, 0.9677 }, { 0.0000, 0.2389, 0.9506 }}, // 30° [255,0,126]
|
||||
{{ 0.9030, 0.8727, 0.9682 }, { 0.3040, 0.0000, 0.9536 }}, // 45° [255,0,190]
|
||||
{{ 0.9270, 0.8727, 0.9685 }, { 0.5426, 0.0000, 0.9570 }}, // 60° [255,0,255]
|
||||
{{ 0.9404, 0.8727, 0.9687 }, { 0.6753, 0.0000, 0.9590 }}, // 75° [190,0,255]
|
||||
{{ 0.9491, 0.8727, 0.9687 }, { 0.7638, 0.0000, 0.9601 }}, // 90° [126,0,255]
|
||||
{{ 0.9539, 0.8727, 0.9689 }, { 0.8115, 0.0000, 0.9609 }}, // 105° [63,0,255]
|
||||
{{ 0.9997, 0.8998, 0.9997 }, { 0.9997, 0.0000, 0.9997 }}, // 120° [0,0,255] (green)
|
||||
{{ 0.9553, 0.8727, 0.9672 }, { 0.8264, 0.0000, 0.9452 }}, // 135° [0,63,255]
|
||||
{{ 0.9555, 0.8727, 0.9621 }, { 0.8266, 0.0000, 0.8937 }}, // 150° [0,126,255]
|
||||
{{ 0.9555, 0.8727, 0.9524 }, { 0.8273, 0.0000, 0.7964 }}, // 165° [0,190,255]
|
||||
{{ 0.9555, 0.8727, 0.9375 }, { 0.8285, 0.0000, 0.6469 }}, // 180° [0,255,255]
|
||||
{{ 0.9557, 0.8727, 0.9091 }, { 0.8301, 0.0000, 0.3648 }}, // 195° [0,255,190]
|
||||
{{ 0.9606, 0.9037, 0.8727 }, { 0.8782, 0.3091, 0.0000 }}, // 210° [0,255,126]
|
||||
{{ 0.9677, 0.9514, 0.8727 }, { 0.9486, 0.7856, 0.0000 }}, // 225° [0,255,63]
|
||||
{{ 0.9997, 0.9997, 0.8998 }, { 0.9997, 0.9997, 0.0000 }}, // 240° [0,255,0] (blue)
|
||||
{{ 0.9652, 0.9652, 0.8727 }, { 0.9245, 0.9252, 0.0000 }}, // 255° [63,255,0]
|
||||
{{ 0.9501, 0.9631, 0.8727 }, { 0.7746, 0.9029, 0.0000 }}, // 270° [126,255,0]
|
||||
{{ 0.9219, 0.9587, 0.8727 }, { 0.4919, 0.8601, 0.0000 }}, // 285° [190,255,0]
|
||||
{{ 0.8786, 0.9521, 0.8727 }, { 0.0584, 0.7946, 0.0000 }}, // 300° [255,255,0]
|
||||
{{ 0.8727, 0.9531, 0.9152 }, { 0.0000, 0.8022, 0.4250 }}, // 315° [255,190,0]
|
||||
{{ 0.8727, 0.9542, 0.9467 }, { 0.0000, 0.8145, 0.7889 }}, // 330° [255,126,0]
|
||||
{{ 0.8728, 0.9547, 0.9631 }, { 0.0000, 0.8207, 0.9044 }} // 345° [255,63,0]
|
||||
}},
|
||||
// Ring 1, min value RGB component value = 35
|
||||
{{
|
||||
{{ 0.8727, 0.9499, 0.9660 }, { 0.0000, 0.7714, 0.9337 }}, // 0° [255,35,39] (red)
|
||||
{{ 0.8727, 0.9255, 0.9665 }, { 0.0000, 0.5268, 0.9365 }}, // 15° [255,35,90]
|
||||
{{ 0.8727, 0.8793, 0.9662 }, { 0.0000, 0.0664, 0.9345 }}, // 30° [255,35,145]
|
||||
{{ 0.9079, 0.8727, 0.9672 }, { 0.3515, 0.0000, 0.9455 }}, // 45° [255,35,200]
|
||||
{{ 0.9270, 0.8727, 0.9680 }, { 0.5429, 0.0000, 0.9519 }}, // 60° [255,35,255]
|
||||
{{ 0.9386, 0.8727, 0.9680 }, { 0.6593, 0.0000, 0.9534 }}, // 75° [200,35,255]
|
||||
{{ 0.9470, 0.8727, 0.9682 }, { 0.7433, 0.0000, 0.9546 }}, // 90° [145,35,255]
|
||||
{{ 0.9524, 0.8727, 0.9682 }, { 0.7961, 0.0000, 0.9552 }}, // 105° [90,35,255]
|
||||
{{ 0.9547, 0.8727, 0.9682 }, { 0.8212, 0.0000, 0.9555 }}, // 120° [39,35,255] (green)
|
||||
{{ 0.9547, 0.8727, 0.9655 }, { 0.8215, 0.0000, 0.9284 }}, // 135° [35,90,255]
|
||||
{{ 0.9550, 0.8727, 0.9600 }, { 0.8217, 0.0000, 0.8712 }}, // 150° [35,145,255]
|
||||
{{ 0.9550, 0.8727, 0.9506 }, { 0.8225, 0.0000, 0.7788 }}, // 165° [35,200,255]
|
||||
{{ 0.9551, 0.8727, 0.9375 }, { 0.8235, 0.0000, 0.6469 }}, // 180° [35,255,255]
|
||||
{{ 0.9547, 0.8727, 0.9150 }, { 0.8212, 0.0000, 0.4226 }}, // 195° [35,255,200]
|
||||
{{ 0.9560, 0.8828, 0.8727 }, { 0.8322, 0.1003, 0.0000 }}, // 210° [35,255,145]
|
||||
{{ 0.9639, 0.9357, 0.8727 }, { 0.9104, 0.6289, 0.0000 }}, // 225° [35,255,90]
|
||||
{{ 0.9675, 0.9610, 0.8727 }, { 0.9475, 0.8803, 0.0000 }}, // 240° [35,255,35] (blue)
|
||||
{{ 0.9596, 0.9597, 0.8727 }, { 0.8686, 0.8686, 0.0000 }}, // 255° [90,255,35]
|
||||
{{ 0.9430, 0.9570, 0.8727 }, { 0.7023, 0.8435, 0.0000 }}, // 270° [145,255,35]
|
||||
{{ 0.9160, 0.9530, 0.8727 }, { 0.4332, 0.8032, 0.0000 }}, // 285° [200,255,35]
|
||||
{{ 0.8780, 0.9472, 0.8728 }, { 0.0518, 0.7458, 0.0000 }}, // 300° [255,255,35]
|
||||
{{ 0.8727, 0.9477, 0.9099 }, { 0.0000, 0.7497, 0.3717 }}, // 315° [255,200,35]
|
||||
{{ 0.8727, 0.9490, 0.9396 }, { 0.0000, 0.7612, 0.6689 }}, // 330° [255,145,35]
|
||||
{{ 0.8727, 0.9496, 0.9580 }, { 0.0000, 0.7683, 0.8512 }} // 345° [255,90,35]
|
||||
}},
|
||||
// Ring 2, min value RGB component value = 73
|
||||
{{
|
||||
{{ 0.8727, 0.9352, 0.9609 }, { 0.0000, 0.6244, 0.8822 }}, // 0° [255,73,76] (red)
|
||||
{{ 0.8727, 0.9035, 0.9616 }, { 0.0000, 0.3068, 0.8888 }}, // 15° [255,73,119]
|
||||
{{ 0.8847, 0.8727, 0.9629 }, { 0.1189, 0.0000, 0.9004 }}, // 30° [255,73,164]
|
||||
{{ 0.9121, 0.8727, 0.9650 }, { 0.3936, 0.0000, 0.9237 }}, // 45° [255,73,209]
|
||||
{{ 0.9270, 0.8727, 0.9665 }, { 0.5431, 0.0000, 0.9367 }}, // 60° [255,73,255]
|
||||
{{ 0.9370, 0.8727, 0.9665 }, { 0.6428, 0.0000, 0.9377 }}, // 75° [209,73,255]
|
||||
{{ 0.9445, 0.8727, 0.9665 }, { 0.7182, 0.0000, 0.9386 }}, // 90° [164,73,255]
|
||||
{{ 0.9499, 0.8727, 0.9667 }, { 0.7722, 0.0000, 0.9391 }}, // 105° [119,73,255]
|
||||
{{ 0.9534, 0.8727, 0.9667 }, { 0.8066, 0.0000, 0.9396 }}, // 120° [73,73,255] (green)
|
||||
{{ 0.9534, 0.8727, 0.9631 }, { 0.8068, 0.0000, 0.9024 }}, // 135° [73,119,255]
|
||||
{{ 0.9536, 0.8727, 0.9570 }, { 0.8073, 0.0000, 0.8435 }}, // 150° [73,164,255]
|
||||
{{ 0.9536, 0.8727, 0.9487 }, { 0.8081, 0.0000, 0.7602 }}, // 165° [73,209,255]
|
||||
{{ 0.9536, 0.8727, 0.9375 }, { 0.8088, 0.0000, 0.6474 }}, // 180° [73,255,255]
|
||||
{{ 0.9526, 0.8727, 0.9201 }, { 0.7990, 0.0000, 0.4736 }}, // 195° [73,255,209]
|
||||
{{ 0.9509, 0.8727, 0.8866 }, { 0.7804, 0.0000, 0.1383 }}, // 210° [73,255,164]
|
||||
{{ 0.9557, 0.9109, 0.8727 }, { 0.8291, 0.3813, 0.0000 }}, // 225° [73,255,119]
|
||||
{{ 0.9606, 0.9455, 0.8727 }, { 0.8798, 0.7268, 0.0000 }}, // 240° [73,255,73] (blue)
|
||||
{{ 0.9499, 0.9439, 0.8727 }, { 0.7714, 0.7107, 0.0000 }}, // 255° [119,255,73]
|
||||
{{ 0.9329, 0.9414, 0.8727 }, { 0.6005, 0.6855, 0.0000 }}, // 270° [164,255,73]
|
||||
{{ 0.9086, 0.9377, 0.8727 }, { 0.5381, 0.6496, 0.0000 }}, // 285° [209,255,73]
|
||||
{{ 0.8760, 0.9329, 0.8727 }, { 0.0314, 0.6013, 0.0000 }}, // 300° [255,255,73]
|
||||
{{ 0.8727, 0.9331, 0.9052 }, { 0.0000, 0.6027, 0.3248 }}, // 315° [255,209,73]
|
||||
{{ 0.8727, 0.9340, 0.9316 }, { 0.0000, 0.6130, 0.5876 }}, // 330° [255,164,73]
|
||||
{{ 0.8727, 0.9347, 0.9499 }, { 0.0000, 0.6202, 0.7717 }} // 345° [255,119,73]
|
||||
}},
|
||||
// Ring 3, min value RGB component value = 109
|
||||
{{
|
||||
{{ 0.8727, 0.9114, 0.9526 }, { 0.0000, 0.3850, 0.7983 }}, // 0° [255,109,112] (red)
|
||||
{{ 0.8727, 0.8788, 0.9541 }, { 0.0000, 0.0615, 0.8125 }}, // 15° [255,109,145]
|
||||
{{ 0.8979, 0.8727, 0.9587 }, { 0.2526, 0.0000, 0.8586 }}, // 30° [255,109,182]
|
||||
{{ 0.9160, 0.8727, 0.9619 }, { 0.4311, 0.0000, 0.8906 }}, // 45° [255,109,218]
|
||||
{{ 0.9272, 0.8727, 0.9639 }, { 0.6254, 0.0000, 0.9111 }}, // 60° [255,109,255]
|
||||
{{ 0.9352, 0.8727, 0.9639 }, { 0.6254, 0.0000, 0.9111 }}, // 75° [218,109,255]
|
||||
{{ 0.9419, 0.8727, 0.9639 }, { 0.6909, 0.0000, 0.9116 }}, // 90° [182,109,255]
|
||||
{{ 0.9472, 0.8727, 0.9639 }, { 0.7438, 0.0000, 0.9119 }}, // 105° [145,109,255]
|
||||
{{ 0.9509, 0.8727, 0.9640 }, { 0.7814, 0.0000, 0.9121 }}, // 120° [109,109,255] (green)
|
||||
{{ 0.9509, 0.7827, 0.9599 }, { 0.7819, 0.0000, 0.7813 }}, // 135° [109,145,255]
|
||||
{{ 0.9509, 0.7827, 0.9541 }, { 0.7824, 0.0000, 0.8135 }}, // 150° [109,182,255]
|
||||
{{ 0.9511, 0.7827, 0.9467 }, { 0.7829, 0.0000, 0.7409 }}, // 165° [109,218,255]
|
||||
{{ 0.9512, 0.7827, 0.9376 }, { 0.7837, 0.0000, 0.6484 }}, // 180° [109,255,255]
|
||||
{{ 0.9494, 0.7827, 0.9245 }, { 0.7661, 0.0000, 0.5188 }}, // 195° [109,255,218]
|
||||
{{ 0.9465, 0.7827, 0.9035 }, { 0.7373, 0.0000, 0.3073 }}, // 210° [109,255,182]
|
||||
{{ 0.9435, 0.8810, 0.8727 }, { 0.7077, 0.0083, 0.0000 }}, // 225° [109,255,145]
|
||||
{{ 0.9491, 0.9190, 0.8727 }, { 0.7633, 0.4624, 0.0000 }}, // 240° [109,255,109] (blue)
|
||||
{{ 0.9372, 0.9171, 0.8727 }, { 0.6443, 0.4450, 0.0000 }}, // 255° [145,255,109]
|
||||
{{ 0.9204, 0.9147, 0.8727 }, { 0.4760, 0.4209, 0.0000 }}, // 270° [182,255,109]
|
||||
{{ 0.8991, 0.9119, 0.8727 }, { 0.2651, 0.3906, 0.0000 }}, // 285° [218,255,109]
|
||||
{{ 0.8727, 0.9081, 0.8730 }, { 0.0000, 0.3540, 0.0034 }}, // 300° [255,255,109]
|
||||
{{ 0.8727, 0.9094, 0.9016 }, { 0.0000, 0.3652, 0.2871 }}, // 315° [255,218,109]
|
||||
{{ 0.8727, 0.9101, 0.9236 }, { 0.0000, 0.3737, 0.5083 }}, // 330° [255,182,109]
|
||||
{{ 0.8727, 0.9109, 0.9411 }, { 0.0000, 0.3805, 0.6833 }} // 345° [255,145,109]
|
||||
}},
|
||||
// Ring 4, min value RGB component value = 145
|
||||
{{
|
||||
{{ 0.8727, 0.8783, 0.9416 }, { 0.0000, 0.0566, 0.6879 }}, // 0° [255,145,147] (red)
|
||||
{{ 0.8916, 0.8727, 0.9484 }, { 0.1869, 0.0000, 0.7575 }}, // 15° [255,145,172]
|
||||
{{ 0.9081, 0.8727, 0.9539 }, { 0.3550, 0.0000, 0.8110 }}, // 30° [255,145,200]
|
||||
{{ 0.9191, 0.8727, 0.9572 }, { 0.4646, 0.0000, 0.8460 }}, // 45° [255,145,227]
|
||||
{{ 0.9272, 0.8727, 0.9600 }, { 0.5444, 0.0000, 0.8712 }}, // 60° [255,145,255]
|
||||
{{ 0.9336, 0.8727, 0.9600 }, { 0.6074, 0.0000, 0.8713 }}, // 75° [227,145,255]
|
||||
{{ 0.9387, 0.8727, 0.9600 }, { 0.6603, 0.0000, 0.8712 }}, // 90° [200,145,255]
|
||||
{{ 0.9435, 0.8727, 0.9600 }, { 0.7071, 0.0000, 0.8712 }}, // 105° [172,145,255]
|
||||
{{ 0.9472, 0.8727, 0.9600 }, { 0.7442, 0.0000, 0.8713 }}, // 120° [145,145,255] (green)
|
||||
{{ 0.9472, 0.8727, 0.9557 }, { 0.7446, 0.0000, 0.8311 }}, // 135° [145,172,255]
|
||||
{{ 0.9472, 0.8727, 0.9506 }, { 0.7451, 0.0000, 0.7798 }}, // 150° [145,200,255]
|
||||
{{ 0.9472, 0.8727, 0.9447 }, { 0.7456, 0.0000, 0.7208 }}, // 165° [145,227,255]
|
||||
{{ 0.9475, 0.8727, 0.9377 }, { 0.7463, 0.0000, 0.6498 }}, // 180° [145,255,255]
|
||||
{{ 0.9450, 0.8727, 0.9286 }, { 0.7228, 0.0000, 0.5588 }}, // 195° [145,255,227]
|
||||
{{ 0.9419, 0.8727, 0.9160 }, { 0.6904, 0.0000, 0.4319 }}, // 210° [145,255,200]
|
||||
{{ 0.9367, 0.8727, 0.8960 }, { 0.6393, 0.0000, 0.2328 }}, // 225° [145,255,172]
|
||||
{{ 0.9316, 0.8793, 0.8727 }, { 0.5890, 0.0666, 0.0000 }}, // 240° [145,255,145] (blue)
|
||||
{{ 0.9199, 0.8778, 0.8727 }, { 0.4716, 0.0505, 0.0000 }}, // 255° [172,255,145]
|
||||
{{ 0.9050, 0.8757, 0.8727 }, { 0.3227, 0.0003, 0.0000 }}, // 270° [200,255,145]
|
||||
{{ 0.8878, 0.8735, 0.8727 }, { 0.1513, 0.0064, 0.0000 }}, // 285° [227,255,145]
|
||||
{{ 0.8727, 0.8760, 0.8781 }, { 0.0000, 0.0316, 0.0528 }}, // 300° [255,255,145]
|
||||
{{ 0.8727, 0.8766, 0.8990 }, { 0.0000, 0.0398, 0.2607 }}, // 315° [255,227,145]
|
||||
{{ 0.8727, 0.8775, 0.9160 }, { 0.0000, 0.0465, 0.4316 }}, // 330° [255,200,145]
|
||||
{{ 0.8727, 0.8778, 0.9306 }, { 0.0000, 0.0523, 0.5798 }} // 345° [255,172,145]
|
||||
}},
|
||||
// Ring 5, min value RGB component value = 181
|
||||
{{
|
||||
{{ 0.8980, 0.8727, 0.9391 }, { 0.5784, 0.4409, 0.8030 }}, // 0° [255,181,182] (red)
|
||||
{{ 0.9079, 0.8727, 0.9445 }, { 0.6330, 0.4409, 0.8327 }}, // 15° [255,181,199]
|
||||
{{ 0.9162, 0.8727, 0.9486 }, { 0.6774, 0.4409, 0.8556 }}, // 30° [255,181,218]
|
||||
{{ 0.9221, 0.8727, 0.9519 }, { 0.7104, 0.4409, 0.8725 }}, // 45° [255,181,236]
|
||||
{{ 0.9272, 0.8727, 0.9545 }, { 0.7382, 0.4409, 0.8866 }}, // 60° [255,181,255]
|
||||
{{ 0.9316, 0.8727, 0.9545 }, { 0.7618, 0.4409, 0.8863 }}, // 75° [236,181,255]
|
||||
{{ 0.9355, 0.8727, 0.9544 }, { 0.7827, 0.4409, 0.8861 }}, // 90° [218,181,255]
|
||||
{{ 0.9391, 0.8727, 0.9542 }, { 0.8025, 0.4409, 0.8859 }}, // 105° [199,181,255]
|
||||
{{ 0.9421, 0.8727, 0.9542 }, { 0.8191, 0.4409, 0.8859 }}, // 120° [181,181,255] (green)
|
||||
{{ 0.9421, 0.8727, 0.9509 }, { 0.8193, 0.4409, 0.8676 }}, // 135° [181,199,255]
|
||||
{{ 0.9421, 0.8727, 0.9470 }, { 0.8195, 0.4409, 0.8458 }}, // 150° [181,218,255]
|
||||
{{ 0.9424, 0.8727, 0.9429 }, { 0.8197, 0.4409, 0.8230 }}, // 165° [181,236,255]
|
||||
{{ 0.9424, 0.8727, 0.9380 }, { 0.8202, 0.4409, 0.7961 }}, // 180° [181,255,255]
|
||||
{{ 0.9399, 0.8727, 0.9321 }, { 0.8071, 0.4409, 0.7651 }}, // 195° [181,255,236]
|
||||
{{ 0.9370, 0.8727, 0.9252 }, { 0.7912, 0.4409, 0.7274 }}, // 210° [181,255,218]
|
||||
{{ 0.9331, 0.8727, 0.9160 }, { 0.7697, 0.4409, 0.6767 }}, // 225° [181,255,199]
|
||||
{{ 0.9282, 0.8727, 0.9045 }, { 0.7431, 0.4409, 0.6135 }}, // 240° [181,255,181] (blue)
|
||||
{{ 0.9221, 0.8727, 0.9050 }, { 0.7094, 0.4409, 0.6164 }}, // 255° [199,255,181]
|
||||
{{ 0.9147, 0.8727, 0.9057 }, { 0.6700, 0.4409, 0.6200 }}, // 270° [218,255,181]
|
||||
{{ 0.9074, 0.8727, 0.9062 }, { 0.6296, 0.4409, 0.6240 }}, // 285° [236,255,181]
|
||||
{{ 0.8988, 0.8727, 0.9072 }, { 0.5834, 0.4409, 0.6282 }}, // 300° [255,255,181]
|
||||
{{ 0.8986, 0.8727, 0.9166 }, { 0.5820, 0.4409, 0.6807 }}, // 315° [255,236,181]
|
||||
{{ 0.8985, 0.8727, 0.9250 }, { 0.5805, 0.4409, 0.7258 }}, // 330° [255,218,181]
|
||||
{{ 0.8981, 0.8727, 0.9329 }, { 0.5793, 0.4409, 0.7687 }} // 345° [255,199,181]
|
||||
}},
|
||||
// Ring 6, min value RGB component value = 219
|
||||
{{
|
||||
{{ 0.9167, 0.8727, 0.9389 }, { 0.4399, 0.0000, 0.6606 }}, // 0° [255,219,219] (red)
|
||||
{{ 0.9199, 0.8727, 0.9414 }, { 0.4711, 0.0000, 0.6850 }}, // 15° [255,219,228]
|
||||
{{ 0.9226, 0.8727, 0.9432 }, { 0.4992, 0.0000, 0.7068 }}, // 30° [255,219,237]
|
||||
{{ 0.9252, 0.8727, 0.9452 }, { 0.5241, 0.0000, 0.7262 }}, // 45° [255,219,246]
|
||||
{{ 0.9275, 0.8727, 0.9472 }, { 0.5465, 0.0000, 0.7437 }}, // 60° [255,219,255]
|
||||
{{ 0.9294, 0.8727, 0.9470 }, { 0.5675, 0.0000, 0.7432 }}, // 75° [246,219,255]
|
||||
{{ 0.9316, 0.8727, 0.9470 }, { 0.5879, 0.0000, 0.7427 }}, // 90° [237,219,255]
|
||||
{{ 0.9335, 0.8727, 0.9470 }, { 0.6074, 0.0000, 0.7424 }}, // 105° [228,219,255]
|
||||
{{ 0.9352, 0.8727, 0.9470 }, { 0.6259, 0.0000, 0.7419 }}, // 120° [219,219,255] (green)
|
||||
{{ 0.9352, 0.8727, 0.9450 }, { 0.6262, 0.0000, 0.7217 }}, // 135° [219,228,255]
|
||||
{{ 0.9355, 0.8727, 0.9429 }, { 0.6264, 0.0000, 0.7002 }}, // 150° [219,237,255]
|
||||
{{ 0.9355, 0.8727, 0.9406 }, { 0.6269, 0.0000, 0.6777 }}, // 165° [219,246,255]
|
||||
{{ 0.9355, 0.8727, 0.9381 }, { 0.6272, 0.0000, 0.6540 }}, // 180° [219,255,255]
|
||||
{{ 0.9339, 0.8727, 0.9357 }, { 0.6111, 0.0000, 0.6291 }}, // 195° [219,255,246]
|
||||
{{ 0.9321, 0.8727, 0.9329 }, { 0.5929, 0.0000, 0.6013 }}, // 210° [219,255,237]
|
||||
{{ 0.9301, 0.8727, 0.9299 }, { 0.5729, 0.0000, 0.5703 }}, // 225° [219,255,228]
|
||||
{{ 0.9277, 0.8727, 0.9263 }, { 0.5503, 0.0000, 0.5354 }}, // 240° [219,255,219] (blue)
|
||||
{{ 0.9252, 0.8727, 0.9265 }, { 0.5249, 0.0000, 0.5368 }}, // 255° [228,255,219]
|
||||
{{ 0.9226, 0.8727, 0.9265 }, { 0.4985, 0.0000, 0.5383 }}, // 270° [237,255,219]
|
||||
{{ 0.9199, 0.8727, 0.9267 }, { 0.4711, 0.0000, 0.5399 }}, // 285° [246,255,219]
|
||||
{{ 0.9170, 0.8727, 0.9270 }, { 0.4426, 0.0000, 0.5414 }}, // 300° [255,255,219]
|
||||
{{ 0.9170, 0.8727, 0.9301 }, { 0.4419, 0.0000, 0.5734 }}, // 315° [255,246,219]
|
||||
{{ 0.9170, 0.8727, 0.9331 }, { 0.4411, 0.0000, 0.6039 }}, // 330° [255,237,219]
|
||||
{{ 0.9167, 0.8727, 0.9360 }, { 0.4406, 0.0000, 0.6330 }} // 345° [255,228,219]
|
||||
}}
|
||||
}};
|
||||
// clang-format on
|
||||
|
||||
/**
|
||||
* This class can handle the GPIO outputs for the RGB light mode,
|
||||
* based on RGB color values + brightness.
|
||||
*/
|
||||
class ColorHandlerRGB : public ColorHandler {
|
||||
public:
|
||||
bool set_light_color_values(light::LightColorValues v) {
|
||||
light_mode = LIGHT_MODE_RGB;
|
||||
|
||||
if (v.get_color_mode() != light::ColorMode::RGB) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Determine the ring level for the color. This is a value between 0
|
||||
// and 7, determining in what ring of the RGB circle the requested
|
||||
// color resides.
|
||||
auto rgb_min = std::min(std::min(v.get_red(), v.get_green()), v.get_blue());
|
||||
auto level = 7.0f * rgb_min;
|
||||
|
||||
// While the default color circle in Home Assistant presents only a
|
||||
// subset of colors, it is possible to request colors outside this
|
||||
// subset as well. Therefore, the ring level might contain a
|
||||
// fractional value instead of a plain integer. To accomodate for
|
||||
// this, interpolation will be done to get the final outputs.
|
||||
|
||||
// Determine duty cycle measurements for the outer ring.
|
||||
auto level_a = floor(level);
|
||||
set_duty_cycles_(&rgbp_a_, level_a, v.get_red(), v.get_green(), v.get_blue(), v.get_brightness(), &rgb_a_);
|
||||
|
||||
// Determine duty cycle measurements for the inner ring.
|
||||
set_duty_cycles_(&rgbp_b_, level_a, v.get_red(), v.get_green(), v.get_blue(), v.get_brightness(), &rgb_b_);
|
||||
|
||||
// Almost there! We now have the correct duty cycles for the
|
||||
// two rings that we were looking at. In this last step, the
|
||||
// two values are interpolated based on the ring level.
|
||||
auto d = level - level_a;
|
||||
red = std::lerp(rgb_a_.red, rgb_b_.red, d);
|
||||
green = std::lerp(rgb_a_.green, rgb_b_.green, d);
|
||||
blue = std::lerp(rgb_a_.blue, rgb_b_.blue, d);
|
||||
|
||||
// The white output channel will always be 0 for RGB.
|
||||
white = 0.0f;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
RGBPoint rgbp_a_;
|
||||
RGBPoint rgbp_b_;
|
||||
RGB rgb_a_;
|
||||
RGB rgb_b_;
|
||||
|
||||
void set_duty_cycles_(RGBPoint *p, int ring_level, float r, float g, float b, float brightness, RGB *rgb) {
|
||||
// Ring level 7 = white light center. The duty cycles for this level
|
||||
// can be computed using a few basic functions.
|
||||
if (ring_level == 7) {
|
||||
rgb->red = 0.932101 - 0.383377 * brightness;
|
||||
rgb->green = 0.883185 - 0.881623 * brightness;
|
||||
rgb->blue = 0.94188 - 0.284498 * brightness;
|
||||
return;
|
||||
}
|
||||
|
||||
// Other ring levels are more complex. Start by retrieving the duty
|
||||
// cycle measurement data for the ring at hand.
|
||||
auto ring = rgb_circle_[ring_level];
|
||||
|
||||
// Because we only have a subset of all colors in the RGB ring
|
||||
// available in the configuration table, some interpolation will
|
||||
// have to be done.
|
||||
// First, compute the position on the ring for the requested RGB
|
||||
// color. This is basically a hue representation of the requested
|
||||
// color. It is expressed as a number of degrees around the ring,
|
||||
// starting with red (at 0°).
|
||||
auto pos = ring_pos_(r, g, b) / 15.0f;
|
||||
|
||||
// Since there are 24 measurements for each ring, each measurement
|
||||
// covers 360°/24 = 15°. Using that knowledge, the measurements to
|
||||
// use for interpolation can be picked from the ring data.
|
||||
auto pos_x = floor(pos);
|
||||
auto x = ring[pos_x];
|
||||
auto pos_y = ceil(pos);
|
||||
auto y = ring[pos_y > 23 ? 0 : pos_y];
|
||||
|
||||
// Interpolate based on the ring position.
|
||||
auto d = pos - pos_x;
|
||||
p->low.red = std::lerp(x.low.red, y.low.red, d);
|
||||
p->low.green = std::lerp(x.low.green, y.low.green, d);
|
||||
p->low.blue = std::lerp(x.low.blue, y.low.blue, d);
|
||||
p->high.red = std::lerp(x.high.red, y.high.red, d);
|
||||
p->high.green = std::lerp(x.high.green, y.high.green, d);
|
||||
p->high.blue = std::lerp(x.high.blue, y.high.blue, d);
|
||||
|
||||
// Interpolate based on brightness level.
|
||||
apply_brightness_(p, brightness, rgb);
|
||||
}
|
||||
|
||||
protected:
|
||||
/**
|
||||
* Returns the position on an RGB ring in degrees (0 - 359).
|
||||
*/
|
||||
float ring_pos_(float red, float green, float blue) {
|
||||
auto rgb_min = std::min(std::min(red, green), blue);
|
||||
auto rgb_max = std::max(std::max(red, green), blue);
|
||||
auto delta = rgb_max - rgb_min;
|
||||
float pos;
|
||||
if (delta == 0.0f)
|
||||
pos = 0.0f;
|
||||
else if (red == rgb_max)
|
||||
pos = 60.0f * fmod((green - blue) / delta, 6);
|
||||
else if (green == rgb_max)
|
||||
pos = 60.0f * ((blue - red) / delta + 2.0f);
|
||||
else
|
||||
pos = 60.0f * ((red - green) / delta + 4.0f);
|
||||
if (pos < 0)
|
||||
pos = pos + 360;
|
||||
return pos;
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply brightness interpolation to the duty cycle measurements. We
|
||||
* have the low (0.01) and high (1.00) brightness measurements in the
|
||||
* data. Brightness can be applied by means of linear interpolation.
|
||||
*/
|
||||
void apply_brightness_(RGBPoint *p, float brightness, RGB *rgb) {
|
||||
auto d = brightness - 0.01f;
|
||||
rgb->red = std::lerp(p->low.red, p->high.red, d);
|
||||
rgb->green = std::lerp(p->low.green, p->high.green, d);
|
||||
rgb->blue = std::lerp(p->low.blue, p->high.blue, d);
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace bslamp2
|
||||
} // namespace xiaomi
|
||||
} // namespace esphome
|
||||
@@ -0,0 +1,40 @@
|
||||
#pragma once
|
||||
|
||||
namespace esphome {
|
||||
namespace xiaomi {
|
||||
namespace bslamp2 {
|
||||
|
||||
/**
|
||||
* This is an interface definition that is used to extend the LightState
|
||||
* class with functionality for disco actions (immediate light updates,
|
||||
* not publishing or saving the light state).
|
||||
*
|
||||
* This interface is required by the DiscoAction class.
|
||||
*/
|
||||
class LightStateDiscoSupport {
|
||||
public:
|
||||
/**
|
||||
* Stop the disco, by restoring the previously remembered light state.
|
||||
*/
|
||||
virtual void disco_stop() = 0;
|
||||
|
||||
/**
|
||||
* Do not wait until the next loop() call for the light to write the
|
||||
* requested state to the light output, but write the new state
|
||||
* right away.
|
||||
*
|
||||
* This allows us to update the state of the light, even when we are
|
||||
* being called in the middle of another component's loop().
|
||||
*/
|
||||
virtual void disco_apply() = 0;
|
||||
|
||||
/**
|
||||
* Create a light::LightCall object, with some properties already
|
||||
* configured for using it as a disco call.
|
||||
*/
|
||||
virtual light::LightCall make_disco_call(bool save_and_publish) = 0;
|
||||
};
|
||||
|
||||
} // namespace bslamp2
|
||||
} // namespace xiaomi
|
||||
} // namespace esphome
|
||||
@@ -0,0 +1,102 @@
|
||||
#pragma once
|
||||
|
||||
#include "../common.h"
|
||||
#include "../light_hal.h"
|
||||
#include "color_handler_chain.h"
|
||||
#include "light_transformer.h"
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/components/ledc/ledc_output.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace xiaomi {
|
||||
namespace bslamp2 {
|
||||
|
||||
/**
|
||||
* A LightOutput class for the Xiaomi Mijia Bedside Lamp 2.
|
||||
*
|
||||
* The function of this class is to translate a required light state
|
||||
* into actual physicial GPIO output signals to drive the device's LED
|
||||
* circuitry. It forms the glue between the physical device and the
|
||||
* logical light color input.
|
||||
*/
|
||||
class XiaomiBslamp2LightOutput : public Component, public light::LightOutput {
|
||||
public:
|
||||
void set_parent(LightHAL *light) { light_ = light; }
|
||||
|
||||
void set_night_light_color_temperature_calibration(float red, float green, float blue) {
|
||||
night_light_calibration_ = {red, green, blue};
|
||||
color_handler_chain.set_night_light_color_temperature_calibration(night_light_calibration_);
|
||||
apply_current_state();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a LightTraits object, which is used to explain to the outside
|
||||
* world (e.g. Home Assistant) what features are supported by this device.
|
||||
*/
|
||||
light::LightTraits get_traits() override {
|
||||
auto traits = light::LightTraits();
|
||||
traits.set_supported_color_modes({light::ColorMode::RGB, light::ColorMode::COLOR_TEMPERATURE});
|
||||
traits.set_min_mireds(MIRED_MIN);
|
||||
traits.set_max_mireds(MIRED_MAX);
|
||||
return traits;
|
||||
}
|
||||
|
||||
std::unique_ptr<light::LightTransformer> create_default_transition() override {
|
||||
return make_unique<XiaomiBslamp2LightTransitionTransformer>(
|
||||
light_,
|
||||
&light_mode_callback_,
|
||||
&state_callback_,
|
||||
night_light_calibration_);
|
||||
}
|
||||
|
||||
void add_on_light_mode_callback(std::function<void(std::string)> &&callback) {
|
||||
light_mode_callback_.add(std::move(callback));
|
||||
}
|
||||
|
||||
void add_on_state_callback(std::function<void(light::LightColorValues)> &&callback) {
|
||||
state_callback_.add(std::move(callback));
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies a requested light state to the physicial GPIO outputs.
|
||||
*/
|
||||
void write_state(light::LightState *state) {
|
||||
state_ = state;
|
||||
auto values = state->current_values;
|
||||
|
||||
color_handler_chain.set_light_color_values(values);
|
||||
light_mode_callback_.call(color_handler_chain.light_mode);
|
||||
state_callback_.call(values);
|
||||
|
||||
// Note: one might think that it is more logical to turn on the LED
|
||||
// circuitry master switch after setting the individual channels,
|
||||
// but this is the order that was used by the original firmware. I
|
||||
// tried to stay as close as possible to the original behavior, so
|
||||
// that's why these GPIOs are turned on at this point.
|
||||
if (values.get_state() != 0)
|
||||
light_->turn_on();
|
||||
|
||||
// Apply the GPIO output levels as defined by the color handler.
|
||||
light_->set_state(&color_handler_chain);
|
||||
|
||||
if (values.get_state() == 0)
|
||||
light_->turn_off();
|
||||
}
|
||||
|
||||
void apply_current_state() {
|
||||
if (state_ != nullptr)
|
||||
write_state(state_);
|
||||
}
|
||||
|
||||
protected:
|
||||
LightHAL *light_;
|
||||
light::LightState *state_{nullptr};
|
||||
ColorHandlerChain color_handler_chain;
|
||||
CallbackManager<void(std::string)> light_mode_callback_{};
|
||||
CallbackManager<void(light::LightColorValues)> state_callback_{};
|
||||
NightLightCalibration night_light_calibration_{DEFAULT_NIGHT_LIGHT_CALIBRATION};
|
||||
};
|
||||
|
||||
} // namespace bslamp2
|
||||
} // namespace xiaomi
|
||||
} // namespace esphome
|
||||
@@ -0,0 +1,66 @@
|
||||
#pragma once
|
||||
|
||||
#include "../common.h"
|
||||
#include "interfaces.h"
|
||||
#include "light_output.h"
|
||||
#include "esphome/components/light/light_state.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace xiaomi {
|
||||
namespace bslamp2 {
|
||||
|
||||
/**
|
||||
* A custom LightState class for the Xiaomi Bedside Lamp 2.
|
||||
*
|
||||
* It is used by the DiscoAction to apply immediate light output
|
||||
* updates, without saving or publishing the new state.
|
||||
*/
|
||||
class XiaomiBslamp2LightState : public light::LightState, public LightStateDiscoSupport {
|
||||
public:
|
||||
XiaomiBslamp2LightState(XiaomiBslamp2LightOutput *output) : light::LightState(output) { }
|
||||
|
||||
void disco_stop() {
|
||||
light::LightStateRTCState recovered{};
|
||||
if (this->rtc_.load(&recovered)) {
|
||||
auto call = make_disco_call(true);
|
||||
call.set_state(recovered.state);
|
||||
call.set_brightness_if_supported(recovered.brightness);
|
||||
call.set_color_brightness_if_supported(recovered.color_brightness);
|
||||
call.set_red_if_supported(recovered.red);
|
||||
call.set_green_if_supported(recovered.green);
|
||||
call.set_blue_if_supported(recovered.blue);
|
||||
call.set_white_if_supported(recovered.white);
|
||||
call.set_color_temperature_if_supported(recovered.color_temp);
|
||||
call.set_cold_white_if_supported(recovered.cold_white);
|
||||
call.set_warm_white_if_supported(recovered.warm_white);
|
||||
if (recovered.effect != 0) {
|
||||
call.set_effect(recovered.effect);
|
||||
} else {
|
||||
call.set_transition_length_if_supported(0);
|
||||
}
|
||||
call.set_color_mode_if_supported(recovered.color_mode);
|
||||
call.perform();
|
||||
}
|
||||
}
|
||||
|
||||
void disco_apply() {
|
||||
this->output_->write_state(this);
|
||||
this->next_write_ = false;
|
||||
}
|
||||
|
||||
void set_night_light_color_temperature_calibration(float red, float green, float blue) {
|
||||
auto *output = static_cast<XiaomiBslamp2LightOutput *>(this->output_);
|
||||
output->set_night_light_color_temperature_calibration(red, green, blue);
|
||||
}
|
||||
|
||||
light::LightCall make_disco_call(bool save_and_publish) {
|
||||
auto call = this->make_call();
|
||||
call.set_save(save_and_publish);
|
||||
call.set_publish(save_and_publish);
|
||||
return call;
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace bslamp2
|
||||
} // namespace xiaomi
|
||||
} // namespace esphome
|
||||
@@ -0,0 +1,109 @@
|
||||
#pragma once
|
||||
|
||||
#include "../common.h"
|
||||
#include "../light_hal.h"
|
||||
#include "color_handler_chain.h"
|
||||
#include "esphome/components/light/light_transformer.h"
|
||||
#include "esphome/components/light/light_color_values.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace xiaomi {
|
||||
namespace bslamp2 {
|
||||
|
||||
/**
|
||||
* A LightTransitionTransformer class for the Xiaomi Mijia Bedside Lamp 2.
|
||||
*/
|
||||
class XiaomiBslamp2LightTransitionTransformer : public light::LightTransitionTransformer {
|
||||
public:
|
||||
explicit XiaomiBslamp2LightTransitionTransformer(
|
||||
LightHAL *light,
|
||||
CallbackManager<void(std::string)> *light_mode_callback,
|
||||
CallbackManager<void(light::LightColorValues)> *state_callback,
|
||||
NightLightCalibration night_light_calibration) :
|
||||
light_(light),
|
||||
light_mode_callback_(light_mode_callback),
|
||||
state_callback_(state_callback),
|
||||
night_light_calibration_(night_light_calibration) { }
|
||||
|
||||
bool is_finished() override {
|
||||
return force_finish_ || get_progress_() >= 1.0f;
|
||||
}
|
||||
|
||||
void start() override {
|
||||
// Determine the GPIO outputs to use for the start and end point.
|
||||
// This light transition transformer will then transition linearly between them.
|
||||
light_->copy_to(&start_);
|
||||
end_.set_night_light_color_temperature_calibration(night_light_calibration_);
|
||||
end_.set_light_color_values(target_values_);
|
||||
|
||||
// Update the light mode of the light HAL to the target state, unless
|
||||
// this is night mode. For night mode, the update is done after the
|
||||
// state has been reached. This makes sure that forcing instant
|
||||
// transitions for night light to night light is only done when the
|
||||
// night light status has actually been reached. E.g. when in RGB mode
|
||||
// and transitioning to night light in 10 seconds, interrupting this
|
||||
// after 5 seconds with a new night light setting should not make the
|
||||
// transition instant.
|
||||
if (end_.light_mode != LIGHT_MODE_NIGHT) {
|
||||
light_->set_light_mode(end_.light_mode);
|
||||
}
|
||||
|
||||
// Run callbacks. These are normally called from the LightOutput, but
|
||||
// since I don't call LightOutput::write_state() from this transformer's
|
||||
// code, these callbacks must be called from this transformer instead.
|
||||
light_mode_callback_->call(end_.light_mode);
|
||||
state_callback_->call(target_values_);
|
||||
}
|
||||
|
||||
optional<light::LightColorValues> apply() override {
|
||||
// When transitioning between off and night mode, or between night mode
|
||||
// light colors, then do this immediately.
|
||||
// The LED driver circuitry is not capable of doing clean color or brightness
|
||||
// transitions at the low levels as used for the night light.
|
||||
if ((end_.light_mode == LIGHT_MODE_NIGHT &&
|
||||
(start_.light_mode == LIGHT_MODE_OFF || start_.light_mode == LIGHT_MODE_NIGHT)) ||
|
||||
(end_.light_mode == LIGHT_MODE_OFF && start_.light_mode == LIGHT_MODE_NIGHT)) {
|
||||
light_->set_state(&end_);
|
||||
if (end_.light_mode != LIGHT_MODE_OFF) {
|
||||
light_->turn_on();
|
||||
}
|
||||
force_finish_ = true;
|
||||
}
|
||||
// Otherwise perform a standard transformation.
|
||||
else {
|
||||
auto smoothed = light::LightTransitionTransformer::smoothed_progress(get_progress_());
|
||||
light_->set_rgbw(
|
||||
std::lerp(start_.red, end_.red, smoothed),
|
||||
std::lerp(start_.green, end_.green, smoothed),
|
||||
std::lerp(start_.blue, end_.blue, smoothed),
|
||||
std::lerp(start_.white, end_.white, smoothed));
|
||||
if (end_.light_mode != LIGHT_MODE_OFF) {
|
||||
light_->turn_on();
|
||||
}
|
||||
}
|
||||
|
||||
if (is_finished()) {
|
||||
light_->set_light_mode(end_.light_mode);
|
||||
if (end_.light_mode == LIGHT_MODE_OFF) {
|
||||
light_->turn_off();
|
||||
}
|
||||
|
||||
return target_values_;
|
||||
} else {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
protected:
|
||||
LightHAL *light_;
|
||||
bool force_finish_{false};
|
||||
GPIOOutputValues start_{};
|
||||
ColorHandlerChain end_{};
|
||||
CallbackManager<void(std::string)> *light_mode_callback_;
|
||||
CallbackManager<void(light::LightColorValues)> *state_callback_;
|
||||
NightLightCalibration night_light_calibration_;
|
||||
};
|
||||
|
||||
} // namespace bslamp2
|
||||
} // namespace xiaomi
|
||||
} // namespace esphome
|
||||
@@ -0,0 +1,190 @@
|
||||
#pragma once
|
||||
|
||||
#include "../common.h"
|
||||
#include "esphome/core/optional.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace xiaomi {
|
||||
namespace bslamp2 {
|
||||
|
||||
class Preset : public Component {
|
||||
public:
|
||||
std::string group_name;
|
||||
std::string name;
|
||||
Preset *next_preset = nullptr;
|
||||
|
||||
explicit Preset(light::LightState *light, std::string group_name, std::string name)
|
||||
: group_name(group_name), name(name), light_state_(light) {}
|
||||
|
||||
void set_transition_length(uint32_t t) { transition_length_ = t; }
|
||||
void set_brightness(float t) { brightness_ = t; }
|
||||
void set_red(float t) { red_ = t; }
|
||||
void set_green(float t) { green_ = t; }
|
||||
void set_blue(float t) { blue_ = t; }
|
||||
void set_color_temperature(float t) { color_temperature_ = t; }
|
||||
void set_effect(const std::string &effect) { effect_ = effect; }
|
||||
|
||||
void apply() {
|
||||
ESP_LOGI(TAG, "Activating light preset: %s/%s", group_name.c_str(), name.c_str());
|
||||
auto call = light_state_->make_call();
|
||||
call.set_state(true);
|
||||
if (transition_length_.has_value())
|
||||
call.set_transition_length(*transition_length_);
|
||||
if (brightness_.has_value())
|
||||
call.set_brightness(*brightness_);
|
||||
if (red_.has_value())
|
||||
call.set_red(*red_);
|
||||
if (green_.has_value())
|
||||
call.set_green(*green_);
|
||||
if (blue_.has_value())
|
||||
call.set_blue(*blue_);
|
||||
if (color_temperature_.has_value())
|
||||
call.set_color_temperature(*color_temperature_);
|
||||
if (effect_.has_value())
|
||||
call.set_effect(*effect_);
|
||||
call.perform();
|
||||
}
|
||||
|
||||
protected:
|
||||
light::LightState *light_state_;
|
||||
optional<uint32_t> transition_length_;
|
||||
optional<float> brightness_;
|
||||
optional<float> red_;
|
||||
optional<float> green_;
|
||||
optional<float> blue_;
|
||||
optional<float> color_temperature_;
|
||||
optional<std::string> effect_;
|
||||
};
|
||||
|
||||
class PresetGroup {
|
||||
public:
|
||||
std::string name;
|
||||
PresetGroup *next_group = nullptr;
|
||||
Preset *first_preset = nullptr;
|
||||
Preset *last_preset = nullptr;
|
||||
Preset *active_preset = nullptr;
|
||||
|
||||
explicit PresetGroup(std::string g_name) : name(g_name) {}
|
||||
|
||||
void add_preset(Preset *p) {
|
||||
if (first_preset == nullptr) {
|
||||
first_preset = last_preset = active_preset = p;
|
||||
} else {
|
||||
last_preset->next_preset = p;
|
||||
last_preset = p;
|
||||
}
|
||||
}
|
||||
|
||||
Preset *get_preset(std::string p_name) {
|
||||
for (auto p = first_preset; p != nullptr; p = p->next_preset)
|
||||
if (p->name == p_name)
|
||||
return p;
|
||||
return nullptr;
|
||||
}
|
||||
};
|
||||
|
||||
class PresetsContainer : public Component {
|
||||
public:
|
||||
PresetGroup *first_group = nullptr;
|
||||
PresetGroup *last_group = nullptr;
|
||||
PresetGroup *active_group = nullptr;
|
||||
|
||||
void dump_config() {
|
||||
if (first_group != nullptr) {
|
||||
ESP_LOGCONFIG(TAG, "Light Presets:");
|
||||
for (auto g = first_group; g != nullptr; g = g->next_group) {
|
||||
ESP_LOGCONFIG(TAG, " Preset group: %s", g->name.c_str());
|
||||
for (auto p = g->first_preset; p != nullptr; p = p->next_preset)
|
||||
ESP_LOGCONFIG(TAG, " Preset: %s", p->name.c_str());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void add_preset(Preset *preset) {
|
||||
auto g = make_preset_group_(preset->group_name);
|
||||
g->add_preset(preset);
|
||||
}
|
||||
|
||||
PresetGroup *get_group(std::string g_name) {
|
||||
for (auto g = first_group; g != nullptr; g = g->next_group)
|
||||
if (g->name == g_name)
|
||||
return g;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void activate_next_group() {
|
||||
if (active_group == nullptr) {
|
||||
ESP_LOGW(TAG, "activate_next_group(): no preset groups defined");
|
||||
return;
|
||||
}
|
||||
active_group = active_group->next_group == nullptr ? first_group : active_group->next_group;
|
||||
if (active_group->active_preset == nullptr) {
|
||||
ESP_LOGW(TAG, "activate_next_group(): no presets defined for group %s", active_group->name.c_str());
|
||||
return;
|
||||
}
|
||||
active_group->active_preset->apply();
|
||||
}
|
||||
|
||||
void activate_next_preset() {
|
||||
if (active_group == nullptr) {
|
||||
ESP_LOGW(TAG, "activate_next_preset(): no preset groups defined");
|
||||
return;
|
||||
}
|
||||
auto p = active_group->active_preset;
|
||||
if (p == nullptr) {
|
||||
ESP_LOGW(TAG, "activate_next_preset(): no presets defined for group %s", active_group->name.c_str());
|
||||
return;
|
||||
}
|
||||
active_group->active_preset = p->next_preset == nullptr ? active_group->first_preset : p->next_preset;
|
||||
active_group->active_preset->apply();
|
||||
}
|
||||
|
||||
void activate_group(std::string g_name) {
|
||||
auto g = get_group(g_name);
|
||||
if (g == nullptr) {
|
||||
ESP_LOGE(TAG, "activate_group(%s): preset group does not exist", g_name.c_str());
|
||||
return;
|
||||
}
|
||||
auto p = g->active_preset;
|
||||
if (p == nullptr) {
|
||||
ESP_LOGW(TAG, "activate_group(%s): no presets defined for group", g_name.c_str());
|
||||
return;
|
||||
}
|
||||
p->apply();
|
||||
}
|
||||
|
||||
void activate_preset(std::string g_name, std::string p_name) {
|
||||
auto g = get_group(g_name);
|
||||
if (g == nullptr) {
|
||||
ESP_LOGE(TAG, "activate_preset(%s, %s): preset group '%s' does not exist", g_name.c_str(), p_name.c_str(),
|
||||
g_name.c_str());
|
||||
return;
|
||||
}
|
||||
auto p = g->get_preset(p_name);
|
||||
if (p == nullptr) {
|
||||
ESP_LOGE(TAG, "activate_preset(%s, %s): preset '%s' does not exist in group '%s'", g_name.c_str(), p_name.c_str(),
|
||||
p_name.c_str(), g->name.c_str());
|
||||
return;
|
||||
}
|
||||
p->apply();
|
||||
}
|
||||
|
||||
protected:
|
||||
PresetGroup *make_preset_group_(std::string g_name) {
|
||||
auto g = get_group(g_name);
|
||||
if (g == nullptr) {
|
||||
g = new PresetGroup(g_name);
|
||||
if (first_group == nullptr) {
|
||||
first_group = last_group = active_group = g;
|
||||
} else {
|
||||
last_group->next_group = g;
|
||||
last_group = g;
|
||||
}
|
||||
}
|
||||
return g;
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace bslamp2
|
||||
} // namespace xiaomi
|
||||
} // namespace esphome
|
||||
Reference in New Issue
Block a user