From 04022ddc2ed552541e1ae2eb36be10dc0b037650 Mon Sep 17 00:00:00 2001 From: Maurice Makaay Date: Sun, 2 May 2021 01:38:40 +0200 Subject: [PATCH] Implement disco-mode actions. (#24) Co-authored-by: Maurice Makaay --- doc/configuration.md | 33 +++++++++++++++++++ light/__init__.py | 52 +++++++++++++++++++++++++++-- light/automation.h | 46 ++++++++++++++++++++++++++ light/color_transition_handler.h | 17 +--------- light/interfaces.h | 56 ++++++++++++++++++++++++++++++++ light/light_state.h | 55 +++++++++++++++++++++++++++++-- 6 files changed, 238 insertions(+), 21 deletions(-) create mode 100644 light/interfaces.h diff --git a/doc/configuration.md b/doc/configuration.md index ee88dcf..678c2b3 100644 --- a/doc/configuration.md +++ b/doc/configuration.md @@ -134,6 +134,39 @@ It is possible to control the night light mode separately. An example of this can be found in the [example.yaml](example.yaml), in which holding the power button is bound to activating the night light. +### light.disco_on Action + +This action sets the state of the light immediately +(i.e. without waiting for the next main loop iteration), +without saving the state to memory and without publishing +the state change. + +```yaml +on_...: + then: + - light.disco_on: + id: my_bedside_lamp + brightness: 80% + red: 70% + green: 0% + blue: 100% +``` + +The possible configuration options for this Action are the same +as those for the standard `light.turn_on` Action. + +### light.disco_off Action + +This action turns off the disco mode by restoring the state +of the lamp to the last known state that was saved to memory. + +```yaml +on_...: + then: + - light.disco_off: + id: my_bedside_lamp +``` + ### Light presets The presets functionality was written with the original lamp firemware diff --git a/light/__init__.py b/light/__init__.py index b550ca6..c10035b 100644 --- a/light/__init__.py +++ b/light/__init__.py @@ -5,8 +5,8 @@ from esphome import automation from esphome.core import coroutine from esphome.const import ( 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 + CONF_STATE, CONF_OUTPUT_ID, CONF_TRIGGER_ID, CONF_ID, + CONF_TRANSITION_LENGTH, CONF_BRIGHTNESS, CONF_EFFECT, CONF_FLASH_LENGTH ) from .. import bslamp2_ns, CODEOWNERS, CONF_LIGHT_HAL_ID, LightHAL @@ -31,6 +31,7 @@ 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({ @@ -125,6 +126,53 @@ def maybe_simple_preset_action(schema): return validator +@automation.register_action( + "light.disco_on", DiscoAction, light.automation.LIGHT_TURN_ON_ACTION_SCHEMA +) +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 +) +def disco_action_off_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) + cg.add(var.set_disco_state(False)) + yield var + @automation.register_action( "preset.activate", ActivatePresetAction, diff --git a/light/automation.h b/light/automation.h index 1a2afdc..8aedfec 100644 --- a/light/automation.h +++ b/light/automation.h @@ -2,7 +2,9 @@ #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 @@ -29,6 +31,50 @@ class BrightnessTrigger : public Trigger { float last_brightness_ = -1.0f; }; +template class DiscoAction : public Action { + 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(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 class ActivatePresetAction : public Action { public: explicit ActivatePresetAction(PresetsContainer *presets) : presets_(presets) {} diff --git a/light/color_transition_handler.h b/light/color_transition_handler.h index 236eb7b..ebf940d 100644 --- a/light/color_transition_handler.h +++ b/light/color_transition_handler.h @@ -2,28 +2,13 @@ #include "../common.h" #include "color_instant_handler.h" +#include "interfaces.h" #include "gpio_outputs.h" namespace esphome { namespace xiaomi { namespace bslamp2 { -/** - * This is an interface definition that is used to extend the LightState - * class with functionality to inspect LightTransformer data from - * within other classes. - * - * This interface is required for the ColorTransitionHandler, so it can - * check whether or not a light color transition is in progress. - */ -class LightStateTransformerInspector { - public: - virtual bool is_active() = 0; - virtual bool is_transition() = 0; - virtual light::LightColorValues get_end_values() = 0; - virtual float get_progress() = 0; -}; - /** * This class is used to handle specific light color transition requirements * for the device. diff --git a/light/interfaces.h b/light/interfaces.h new file mode 100644 index 0000000..f9e47cb --- /dev/null +++ b/light/interfaces.h @@ -0,0 +1,56 @@ +#pragma once + +namespace esphome { +namespace xiaomi { +namespace bslamp2 { + +/** + * This is an interface definition that is used to extend the LightState + * class with functionality to inspect LightTransformer data from + * within other classes. + * + * This interface is required for the ColorTransitionHandler, so it can + * check whether or not a light color transition is in progress. + */ +class LightStateTransformerInspector { + public: + virtual bool is_active() = 0; + virtual bool is_transition() = 0; + virtual light::LightColorValues get_end_values() = 0; + virtual float get_progress() = 0; +}; + +/** + * 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 diff --git a/light/light_state.h b/light/light_state.h index 3471a80..5ff05b4 100644 --- a/light/light_state.h +++ b/light/light_state.h @@ -1,19 +1,36 @@ #pragma once #include "../common.h" +#include "interfaces.h" +#include "esphome/components/light/light_state.h" namespace esphome { namespace xiaomi { namespace bslamp2 { +// Can be replaced with light::LightStateRTCState once pull request +// https://github.com/esphome/esphome/pull/1735 is merged. +struct MyLightStateRTCState { + bool state{false}; + float brightness{1.0f}; + float red{1.0f}; + float green{1.0f}; + float blue{1.0f}; + float white{1.0f}; + float color_temp{1.0f}; + uint32_t effect{0}; +}; + /** - * This custom LightState class is used to provide access to the protected - * LightTranformer information in the LightState class. + * A custom LightState class for the Xiaomi Bedside Lamp 2. * * This class is used by the ColorTransitionHandler class to inspect if * an ongoing light color transition is active in the LightState object. + * + * It is also used by the DiscoAction to apply immediate light output + * updates, without saving or publishing the new state. */ -class XiaomiBslamp2LightState : public light::LightState, public LightStateTransformerInspector { +class XiaomiBslamp2LightState : public light::LightState, public LightStateTransformerInspector, public LightStateDiscoSupport { public: XiaomiBslamp2LightState(const std::string &name, XiaomiBslamp2LightOutput *output) : light::LightState(name, output) { output->set_transformer_inspector(this); @@ -23,6 +40,38 @@ class XiaomiBslamp2LightState : public light::LightState, public LightStateTrans bool is_transition() { return transformer_->is_transition(); } light::LightColorValues get_end_values() { return transformer_->get_end_values(); } float get_progress() { return transformer_->get_progress(); } + + void disco_stop() { + MyLightStateRTCState 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_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); + if (recovered.effect != 0) { + call.set_effect(recovered.effect); + } else { + call.set_transition_length_if_supported(0); + } + call.perform(); + } + } + + void disco_apply() { + this->output_->write_state(this); + this->next_write_ = false; + } + + 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