Browse Source

Add support for presets (#9)

Implemented support for presets.
pull/17/head
Maurice Makaay 3 years ago
committed by GitHub
parent
commit
f85ff71d43
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 487 additions and 55 deletions
  1. +2
    -1
      common.h
  2. +3
    -5
      doc/FLASHING.md
  3. +33
    -8
      doc/example.yaml
  4. +2
    -2
      front_panel_hal.h
  5. +154
    -8
      light/__init__.py
  6. +30
    -0
      light/automation.h
  7. +1
    -0
      light/color_night_light.h
  8. +1
    -3
      light/color_off.h
  9. +1
    -0
      light/color_rgb_light.h
  10. +1
    -0
      light/color_white_light.h
  11. +2
    -7
      light/gpio_outputs.h
  12. +15
    -0
      light/light_modes.h
  13. +0
    -20
      light/light_output.h
  14. +31
    -0
      light/light_state.h
  15. +210
    -0
      light/presets.h
  16. +1
    -1
      sensor/slider_sensor.h

+ 2
- 1
common.h View File

@ -4,8 +4,9 @@ namespace esphome {
namespace xiaomi {
namespace bslamp2 {
// Used for logging purposes.
static const char *TAG = "xiaomi_bslamp2";
} // namespace bslamp2
} // namespace xiaomi
} // namespace esphome

+ 3
- 5
doc/FLASHING.md View File

@ -138,9 +138,9 @@ reconnect the power to boot into the restored firmware.
## Flash new firmware
Setup an ESPHome Project, see [README.md](../README.md)
Compile the firmware for the device and download the `firmware.bin` file
to the device to which the serial adapter is connected.
Setup an ESPHome Project (see [README.md](../README.md)),compile the firmware
for the device and download the `firmware.bin` file to the device to which
the serial adapter is connected.
You can flash the device using esphome or esptool.
I normally use the [esphome-flasher](https://github.com/esphome/esphome-flasher)
@ -187,5 +187,3 @@ https://github.com/arendst/Tasmota/tree/firmware/firmware/tasmota32/ESP32_needed
(remember that the [esphome-flasher](https://github.com/esphome/esphome-flasher)
will give you a bit less of a hard-core experience during flashing)

+ 33
- 8
doc/example.yaml View File

@ -94,6 +94,26 @@ light:
name: "Fast Random"
transition_length: 3s
update_interval: 3s
# You can define one or more groups of presets. These presets can
# be activated using various "preset.activate" action options.
# The presets can for example be used to mimic the behavior of the
# original firmware (tapping the color button = go to next preset,
# holding the color button = switch between RGB and white light mode).
# These bindings have been setup below, using the binary_sensor for
# the color button.
presets:
rgb:
red: { red: 100%, green: 0%, blue: 0% }
green: { red: 0%, green: 100%, blue: 0% }
blue: { red: 0%, green: 0%, blue: 100% }
yellow: { red: 100%, green: 100%, blue: 0% }
purple: { red: 100%, green: 0%, blue: 100% }
randomize: { effect: Fast Random }
white:
cold: { color_temperature: 153 mireds }
chilly: { color_temperature: 275 mireds }
luke: { color_temperature: 400 mireds }
warm: { color_temperature: 588 mireds }
# This text sensor propagates the currently active light mode.
# The possible light modes are: "off", "rgb", "white" and "night".
@ -135,17 +155,22 @@ binary_sensor:
blue: 1
brightness: 0.01
# When touching the color button, set a random color.
# When touching the color button, acivate the next preset.
# When holding the color button, activate the next preset group.
- platform: xiaomi_bslamp2
id: ${id_color_button}
part: color button
on_press:
then:
- light.turn_on:
id: ${id_light}
red: !lambda return random_float();
green: !lambda return random_float();
blue: !lambda return random_float();
on_multi_click:
- timing:
- ON for at most 0.6s
then:
- preset.activate:
next: preset
- timing:
- ON for at least 0.6s
then:
- preset.activate:
next: group
# This sensor component publishes touch events for the front panel slider.
# The published value represents the level at which the slider was touched.


+ 2
- 2
front_panel_hal.h View File

@ -185,8 +185,8 @@ public:
}
void dump_config() {
ESP_LOGCONFIG(TAG, "I2C interrupt");
LOG_PIN(" Interrupt pin: ", trigger_pin_);
ESP_LOGCONFIG(TAG, "FrontPanelHAL:");
LOG_PIN(" I2C interrupt pin: ", trigger_pin_);
}
void loop() {


+ 154
- 8
light/__init__.py View File

@ -2,9 +2,11 @@ import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import light
from esphome import automation
from esphome.core import coroutine
from esphome.const import (
CONF_RED, CONF_GREEN, CONF_BLUE, CONF_WHITE,
CONF_OUTPUT_ID, CONF_TRIGGER_ID, CONF_ID
CONF_RED, CONF_GREEN, CONF_BLUE, CONF_WHITE, CONF_COLOR_TEMPERATURE,
CONF_OUTPUT_ID, CONF_TRIGGER_ID, CONF_ID,
CONF_TRANSITION_LENGTH, CONF_BRIGHTNESS, CONF_EFFECT
)
from .. import bslamp2_ns, CODEOWNERS, CONF_LIGHT_HAL_ID, LightHAL
@ -13,14 +15,54 @@ AUTO_LOAD = ["xiaomi_bslamp2"]
CONF_MASTER1 = "master1"
CONF_MASTER2 = "master2"
CONF_ON_BRIGHTNESS = "on_brightness"
CONF_PRESET_ID = "preset_id"
CONF_PRESETS_ID = "presets_id"
CONF_PRESETS = "presets"
CONF_NEXT = "next"
CONF_GROUP = "group"
CONF_PRESET = "preset"
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)
PRESETS_SCHEMA = cv.Schema({
str.lower: cv.Schema({
str.lower: light.automation.LIGHT_TURN_ON_ACTION_SCHEMA
})
})
PRESET_SCHEMA_BASE = cv.Schema(
{
cv.GenerateID(CONF_ID): cv.use_id(XiaomiBslamp2LightState),
cv.GenerateID(CONF_PRESET_ID): cv.declare_id(Preset),
}
)
PRESET_SCHEMA = cv.Any(
PRESET_SCHEMA_BASE.extend({
cv.Required(CONF_EFFECT): cv.string
}),
PRESET_SCHEMA_BASE.extend({
cv.Required(CONF_COLOR_TEMPERATURE): cv.color_temperature,
cv.Optional(CONF_BRIGHTNESS): cv.percentage,
cv.Optional(CONF_TRANSITION_LENGTH): cv.positive_time_period_milliseconds,
}),
PRESET_SCHEMA_BASE.extend({
cv.Required(CONF_RED): cv.percentage,
cv.Required(CONF_GREEN): cv.percentage,
cv.Required(CONF_BLUE): cv.percentage,
cv.Optional(CONF_BRIGHTNESS): cv.percentage,
cv.Optional(CONF_TRANSITION_LENGTH): cv.positive_time_period_milliseconds,
}),
)
CONFIG_SCHEMA = light.RGB_LIGHT_SCHEMA.extend(
{
cv.GenerateID(): cv.declare_id(XiaomiBslamp2LightState),
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_ON_BRIGHTNESS): automation.validate_automation(
@ -28,16 +70,120 @@ CONFIG_SCHEMA = light.RGB_LIGHT_SCHEMA.extend(
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 to_code(config):
var = cg.new_Pvariable(config[CONF_OUTPUT_ID])
yield light.register_light(var, config)
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()
conf = {}
if value == "next_group":
conf[CONF_NEXT] = CONF_GROUP
elif value == "next_preset":
conf[CONF_NEXT] = CONF_PRESET
elif "." not in value:
conf[CONF_GROUP] = value
else:
group, preset = value.split(".", 2)
conf[CONF_GROUP] = group
conf[CONF_PRESET] = preset
return schema(conf)
return validator
@automation.register_action(
"preset.activate",
ActivatePresetAction,
cv.Schema(
maybe_simple_preset_action(cv.Any(
cv.Schema({
cv.GenerateID(CONF_PRESETS_ID): cv.use_id(PresetsContainer),
cv.Required(CONF_GROUP): is_preset_group,
cv.Optional(CONF_PRESET): is_preset
}),
cv.Schema({
cv.GenerateID(CONF_PRESETS_ID): cv.use_id(PresetsContainer),
cv.Required(CONF_NEXT): cv.one_of(CONF_GROUP, CONF_PRESET, lower=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"))
cg.add(action_var.set_group(config[CONF_GROUP]))
cg.add(action_var.set_preset(config[CONF_PRESET]))
else:
cg.add(action_var.set_operation("activate_group"))
cg.add(action_var.set_group(config[CONF_GROUP]))
yield action_var
@coroutine
def light_output_to_code(config):
light_output_var = cg.new_Pvariable(config[CONF_OUTPUT_ID])
yield light.register_light(light_output_var, config)
light_hal_var = yield cg.get_variable(config[CONF_LIGHT_HAL_ID])
cg.add(var.set_parent(light_hal_var))
cg.add(light_output_var.set_parent(light_hal_var))
@coroutine
def on_brightness_to_code(config):
light_output_var = yield cg.get_variable(config[CONF_OUTPUT_ID])
for conf in config.get(CONF_ON_BRIGHTNESS, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], light_output_var)
yield automation.build_automation(trigger, [(float, "x")], conf)
@coroutine
def preset_to_code(config, preset_group, preset_name):
light_var = yield 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"))
yield cg.register_component(preset_var, config)
@coroutine
def presets_to_code(config):
presets_var = cg.new_Pvariable(config[CONF_PRESETS_ID])
yield 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 = yield preset_to_code(preset_config, preset_group, preset_name)
cg.add(presets_var.add_preset(preset))
def to_code(config):
yield light_output_to_code(config)
yield on_brightness_to_code(config)
yield presets_to_code(config)

+ 30
- 0
light/automation.h View File

@ -4,6 +4,7 @@
#include "esphome/core/component.h"
#include "esphome/core/automation.h"
#include "light_output.h"
#include "presets.h"
namespace esphome {
namespace xiaomi {
@ -28,6 +29,35 @@ protected:
float last_brightness_ = -1.0f;
};
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(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

+ 1
- 0
light/color_night_light.h View File

@ -1,6 +1,7 @@
#pragma once
#include "../common.h"
#include "light_modes.h"
#include "gpio_outputs.h"
namespace esphome {


+ 1
- 3
light/color_off.h View File

@ -1,9 +1,7 @@
#pragma once
#include <array>
#include <stdexcept>
#include "../common.h"
#include "light_modes.h"
#include "gpio_outputs.h"
namespace esphome {


+ 1
- 0
light/color_rgb_light.h View File

@ -4,6 +4,7 @@
#include <cmath>
#include "../common.h"
#include "light_modes.h"
#include "gpio_outputs.h"
namespace esphome {


+ 1
- 0
light/color_white_light.h View File

@ -4,6 +4,7 @@
#include <stdexcept>
#include "../common.h"
#include "light_modes.h"
#include "gpio_outputs.h"
namespace esphome {


+ 2
- 7
light/gpio_outputs.h View File

@ -1,16 +1,11 @@
#pragma once
#include "light_modes.h"
namespace esphome {
namespace xiaomi {
namespace bslamp2 {
// Light modes that can be reported by implementations of GPIOOutputs.
static const std::string LIGHT_MODE_UNKNOWN { "unknown" };
static const std::string LIGHT_MODE_OFF { "off" };
static const std::string LIGHT_MODE_RGB { "rgb" };
static const std::string LIGHT_MODE_WHITE { "white" };
static const std::string LIGHT_MODE_NIGHT { "night" };
/**
* This abstract class is used for implementing classes that translate
* LightColorValues into the required GPIO PWM duty cycle levels to represent


+ 15
- 0
light/light_modes.h View File

@ -0,0 +1,15 @@
#pragma once
namespace esphome {
namespace xiaomi {
namespace bslamp2 {
static const std::string LIGHT_MODE_UNKNOWN { "unknown" };
static const std::string LIGHT_MODE_OFF { "off" };
static const std::string LIGHT_MODE_RGB { "rgb" };
static const std::string LIGHT_MODE_WHITE { "white" };
static const std::string LIGHT_MODE_NIGHT { "night" };
} // namespace bslamp2
} // namespace xiaomi
} // namespace esphome

+ 0
- 20
light/light_output.h View File

@ -108,26 +108,6 @@ protected:
transition_handler_ = new ColorTransitionHandler(exposer);
}
};
/**
* This custom LightState class is used to provide access to the protected
* LightTranformer information in the LightState class.
*
* This class is used by the ColorTransitionHandler class to inspect if
* an ongoing light color transition is active in a LightState object.
*/
class XiaomiBslamp2LightState : public light::LightState, public LightStateTransformerInspector
{
public:
XiaomiBslamp2LightState(const std::string &name, XiaomiBslamp2LightOutput *output) : light::LightState(name, output) {
output->set_transformer_inspector(this);
}
bool is_active() { return transformer_ != nullptr; }
bool is_transition() { return transformer_->is_transition(); }
light::LightColorValues get_end_values() { return transformer_->get_end_values(); }
float get_progress() { return transformer_->get_progress(); }
};
} // namespace bslamp2
} // namespace xiaomi


+ 31
- 0
light/light_state.h View File

@ -0,0 +1,31 @@
#pragma once
#include "../common.h"
namespace esphome {
namespace xiaomi {
namespace bslamp2 {
/**
* This custom LightState class is used to provide access to the protected
* LightTranformer information in the LightState class.
*
* This class is used by the ColorTransitionHandler class to inspect if
* an ongoing light color transition is active in the LightState object.
*/
class XiaomiBslamp2LightState : public light::LightState, public LightStateTransformerInspector
{
public:
XiaomiBslamp2LightState(const std::string &name, XiaomiBslamp2LightOutput *output) : light::LightState(name, output) {
output->set_transformer_inspector(this);
}
bool is_active() { return transformer_ != nullptr; }
bool is_transition() { return transformer_->is_transition(); }
light::LightColorValues get_end_values() { return transformer_->get_end_values(); }
float get_progress() { return transformer_->get_progress(); }
};
} // namespace bslamp2
} // namespace xiaomi
} // namespace esphome

+ 210
- 0
light/presets.h View File

@ -0,0 +1,210 @@
#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) {
return;
}
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

+ 1
- 1
sensor/slider_sensor.h View File

@ -48,7 +48,7 @@ public:
}
void dump_config() {
ESP_LOGCONFIG(TAG, "Front Panel slider sensor:");
ESP_LOGCONFIG(TAG, "Front panel slider sensor:");
ESP_LOGCONFIG(TAG, " Range from: %f", range_from_);
ESP_LOGCONFIG(TAG, " Range to: %f", range_to_);
}


Loading…
Cancel
Save