From e5612e823507600a8cdd1eb7813424fef4266bfe Mon Sep 17 00:00:00 2001 From: Maurice Makaay Date: Sun, 11 Apr 2021 19:04:22 +0200 Subject: [PATCH] What's better than one hub? Two HALs! I split up the HUB component into two separated components with the same kind of function: LightHAL and FrontPanelHAL. --- __init__.py | 159 +++++++++++++++++++------------------------ front_panel_hal.h | 62 +++++++++++++++++ light/__init__.py | 25 +++---- light/light_output.h | 12 ++-- light_hal.h | 48 +++++++++++++ yeelight_bs2_hub.h | 113 ------------------------------ 6 files changed, 200 insertions(+), 219 deletions(-) create mode 100644 front_panel_hal.h create mode 100644 light_hal.h delete mode 100644 yeelight_bs2_hub.h diff --git a/__init__.py b/__init__.py index 5a428fb..f88e4dc 100644 --- a/__init__.py +++ b/__init__.py @@ -1,28 +1,19 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome import pins -from esphome.components.ledc.output import LEDCOutput, validate_frequency +from esphome.components.ledc.output import LEDCOutput from esphome.components.gpio.output import GPIOBinaryOutput -from esphome.components.i2c import I2CComponent +from esphome.components.i2c import I2CComponent, I2CDevice from esphome.core import coroutine from esphome.core import CORE from esphome.const import ( - CONF_ID, - CONF_RED, - CONF_GREEN, - CONF_BLUE, - CONF_WHITE, - CONF_TRIGGER_PIN, - CONF_SDA, - CONF_SCL, - CONF_OUTPUT_ID, - CONF_TRIGGER_ID, - CONF_PIN, - CONF_FREQUENCY, - CONF_CHANNEL, - CONF_PLATFORM, + CONF_ID, CONF_RED, CONF_GREEN, CONF_BLUE, CONF_WHITE, CONF_TRIGGER_PIN, + CONF_SDA, CONF_SCL, CONF_OUTPUT_ID, CONF_TRIGGER_ID, CONF_PIN, + CONF_FREQUENCY, CONF_CHANNEL, CONF_PLATFORM, ) +CODEOWNERS = ["@mmakaay"] + CONF_HUB_ID = "yeelight_bs2_hub_id" CONF_RED_ID = "red_id" CONF_GREEN_ID = "green_id" @@ -33,105 +24,92 @@ CONF_MASTER1_ID = "master1_id" CONF_MASTER2 = "master2" CONF_MASTER2_ID = "master2_id" CONF_FP_I2C_ID = "front_panel_i2c_id" -CONF_ON_BRIGHTNESS = "on_brightness" +CONF_LIGHT_HAL_ID = "light_hal" +CONF_FRONT_PANEL_HAL_ID = "front_panel_hal_id" -CODEOWNERS = ["@mmakaay"] +CONF_ON_BRIGHTNESS = "on_brightness" AUTO_LOAD = ["ledc", "output", "i2c"] -PINS = { - # Config key TYPE, ID GPIO, PARAMS - CONF_RED : ( LEDCOutput, CONF_RED_ID, "GPIO13", 3000, 0 ), - CONF_GREEN : ( LEDCOutput, CONF_GREEN_ID, "GPIO14", 3000, 1 ), - CONF_BLUE : ( LEDCOutput, CONF_BLUE_ID, "GPIO5", 3000, 2 ), - CONF_WHITE : ( LEDCOutput, CONF_WHITE_ID, "GPIO12", 10000, 4 ), - CONF_MASTER1 : ( GPIOBinaryOutput, CONF_MASTER1_ID, "GPIO33" ), - CONF_MASTER2 : ( GPIOBinaryOutput, CONF_MASTER2_ID, "GPIO4" ) -} - -FRONT_PANEL = { - CONF_SDA: "GPIO21", - CONF_SCL: "GPIO19", - CONF_TRIGGER_PIN: "GPIO16" -} - - yeelight_ns = cg.esphome_ns.namespace("yeelight") bs2_ns = yeelight_ns.namespace("bs2") -YeelightBS2Hub = bs2_ns.class_("YeelightBS2Hub", cg.Component) +LightHAL = bs2_ns.class_("LightHAL", cg.Component) +FrontPanelHAL = bs2_ns.class_("FrontPanelHAL", cg.Component, I2CDevice) def make_config_schema(): schema = cv.COMPONENT_SCHEMA.extend({ - cv.GenerateID(): cv.declare_id(YeelightBS2Hub), + # RGBWW Light + cv.GenerateID(CONF_LIGHT_HAL_ID): cv.declare_id(LightHAL), + cv.GenerateID(CONF_RED_ID): cv.declare_id(LEDCOutput), + cv.Optional(CONF_RED, default="GPIO13"): pins.validate_gpio_pin, + cv.GenerateID(CONF_GREEN_ID): cv.declare_id(LEDCOutput), + cv.Optional(CONF_GREEN, default="GPIO14"): pins.validate_gpio_pin, + cv.GenerateID(CONF_BLUE_ID): cv.declare_id(LEDCOutput), + cv.Optional(CONF_BLUE, default="GPIO5"): pins.validate_gpio_pin, + cv.GenerateID(CONF_WHITE_ID): cv.declare_id(LEDCOutput), + cv.Optional(CONF_WHITE, default="GPIO12"): pins.validate_gpio_pin, + cv.GenerateID(CONF_MASTER1_ID): cv.declare_id(GPIOBinaryOutput), + cv.Optional(CONF_MASTER1, default="GPIO33"): pins.validate_gpio_pin, + cv.GenerateID(CONF_MASTER2_ID): cv.declare_id(GPIOBinaryOutput), + cv.Optional(CONF_MASTER2, default="GPIO4"): pins.validate_gpio_pin, + + # Front panel I2C + cv.GenerateID(CONF_FRONT_PANEL_HAL_ID): cv.declare_id(FrontPanelHAL), cv.GenerateID(CONF_FP_I2C_ID): cv.use_id(I2CComponent), - cv.Optional(CONF_SDA, default=FRONT_PANEL[CONF_SDA]): pins.validate_gpio_pin, - cv.Optional(CONF_SCL, default=FRONT_PANEL[CONF_SCL]): pins.validate_gpio_pin, - cv.Optional(CONF_TRIGGER_PIN, default=FRONT_PANEL[CONF_TRIGGER_PIN]): cv.All( + cv.Optional(CONF_SDA, default="GPIO21"): pins.validate_gpio_pin, + cv.Optional(CONF_SCL, default="GPIO19"): pins.validate_gpio_pin, + cv.Optional(CONF_TRIGGER_PIN, default="GPIO16"): cv.All( pins.validate_gpio_pin, pins.validate_has_interrupt ), }) - for key, pin_config in PINS.items(): - type_, id_, pin, *_ = pin_config - schema = schema.extend({ - cv.GenerateID(id_): cv.declare_id(type_), - cv.Optional(key, default=pin): pins.validate_gpio_pin - }) - return schema; CONFIG_SCHEMA = make_config_schema() @coroutine -def make_gpio_pin(key, config): - type_, id_, *_ = PINS[key] - yield from cg.gpio_pin_expression({ - "number": config[key], - "mode": "OUTPUT" - }); +def make_gpio(number, mode="OUTPUT"): + yield from cg.gpio_pin_expression({ "number": number, "mode": mode }); @coroutine -def make_gpio_binary_output(key, config, gpio_var): - type_, id_, *_ = PINS[key] - output_var = cg.new_Pvariable(config[id_]) +def make_gpio_binary_output(id_, number): + gpio_var = yield make_gpio(number) + output_var = cg.new_Pvariable(id_) cg.add(output_var.set_pin(gpio_var)) yield from cg.register_component(output_var, {}) @coroutine -def make_ledc_output(key, config, gpio_var): - type_, id_, _, frequency, channel = PINS[key] - ledc_var = cg.new_Pvariable(config[id_], gpio_var) +def make_ledc_output(id_, number, frequency, channel): + gpio_var = yield make_gpio(number) + ledc_var = cg.new_Pvariable(id_, gpio_var) cg.add(ledc_var.set_frequency(frequency)); cg.add(ledc_var.set_channel(channel)); yield from cg.register_component(ledc_var, {}) -def to_code(config): - # Dirty little hack to make the ESPHome component loader inlcude - # the code for the "gpio" platform for the "output" domain. - # Loading specific platform components is not possible using - # the AUTO_LOAD feature unfortunately. - CORE.config["output"].append({ CONF_PLATFORM: "gpio" }) +@coroutine +def make_light_hal(config): + r_var = yield make_ledc_output(config[CONF_RED_ID], config[CONF_RED], 3000, 0) + g_var = yield make_ledc_output(config[CONF_GREEN_ID], config[CONF_GREEN], 3000, 1) + b_var = yield make_ledc_output(config[CONF_BLUE_ID], config[CONF_BLUE], 3000, 2) + w_var = yield make_ledc_output(config[CONF_WHITE_ID], config[CONF_WHITE], 10000, 4) + m1_var = yield make_gpio_binary_output(config[CONF_MASTER1_ID], config[CONF_MASTER1]) + m2_var = yield make_gpio_binary_output(config[CONF_MASTER2_ID], config[CONF_MASTER2]) + light_hal = cg.new_Pvariable(config[CONF_LIGHT_HAL_ID]) + yield cg.register_component(light_hal, config) + cg.add(light_hal.set_red_pin(r_var)) + cg.add(light_hal.set_green_pin(g_var)) + cg.add(light_hal.set_blue_pin(b_var)) + cg.add(light_hal.set_white_pin(w_var)) + cg.add(light_hal.set_master1_pin(m1_var)) + cg.add(light_hal.set_master2_pin(m2_var)) - hub_var = cg.new_Pvariable(config[CONF_ID]) - yield cg.register_component(hub_var, config) - - for key in PINS: - type_ = PINS[key][0] - gpio_var = yield make_gpio_pin(key, config) - if type_ == LEDCOutput: - pin_var = yield make_ledc_output(key, config, gpio_var) - if type_ == GPIOBinaryOutput: - pin_var = yield make_gpio_binary_output(key, config, gpio_var) - setter = getattr(hub_var, "set_%s_pin" % key) - cg.add(setter(pin_var)) - - trigger_pin = yield cg.gpio_pin_expression({ - "number": config[CONF_TRIGGER_PIN], - "mode": "INPUT", - "inverted": False - }) - cg.add(hub_var.set_trigger_pin(trigger_pin)) +@coroutine +def make_front_panel_hal(config): + trigger_pin = yield make_gpio(config[CONF_TRIGGER_PIN], "INPUT") + fp_hal = cg.new_Pvariable(config[CONF_FRONT_PANEL_HAL_ID]) + yield cg.register_component(fp_hal, config) + cg.add(fp_hal.set_trigger_pin(trigger_pin)) # The i2c component automatically sets up one I2C bus. # Take that bus and update is to make it work for the @@ -140,9 +118,14 @@ def to_code(config): cg.add(fp_i2c_var.set_sda_pin(config[CONF_SDA])) cg.add(fp_i2c_var.set_scl_pin(config[CONF_SCL])) cg.add(fp_i2c_var.set_scan(True)) - cg.add(hub_var.set_front_panel_i2c(fp_i2c_var)) + cg.add(fp_hal.set_i2c_parent(fp_i2c_var)) - for conf in config.get(CONF_ON_BRIGHTNESS, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - yield automation.build_automation(trigger, [(float, "x")], conf) +def to_code(config): + # Dirty little hack to make the ESPHome component loader inlcude + # the code for the "gpio" platform for the "output" domain. + # Loading specific platform components is not possible using + # the AUTO_LOAD feature unfortunately. + CORE.config["output"].append({ CONF_PLATFORM: "gpio" }) + yield make_light_hal(config) + yield make_front_panel_hal(config) diff --git a/front_panel_hal.h b/front_panel_hal.h new file mode 100644 index 0000000..fdd3a82 --- /dev/null +++ b/front_panel_hal.h @@ -0,0 +1,62 @@ +#pragma once + +#include "common.h" +#include "esphome/core/component.h" +#include "esphome/core/esphal.h" +#include "esphome/components/i2c/i2c.h" + +namespace esphome { +namespace yeelight { +namespace bs2 { + +class FrontPanelHAL : public Component, public i2c::I2CDevice { +public: + void set_trigger_pin(GPIOPin *pin) { i2c_trigger_pin_ = pin; } + + void setup() { + ESP_LOGCONFIG(TAG, "Setting up I2C trigger pin interrupt..."); + this->i2c_trigger_pin_->setup(); + this->i2c_trigger_pin_->attach_interrupt( + FrontPanelHAL::isr, this, FALLING); + } + + void dump_config() { + ESP_LOGCONFIG(TAG, "I2C"); + LOG_PIN(" Interrupt pin: ", this->i2c_trigger_pin_); + } + + void loop() { + if (this->queue_length > 0) { + this->queue_length--; + ESP_LOGD(TAG, "EVENT!"); + } + } + +protected: + // The GPIO pin that is used by the front panel to notify the ESP that + // a touch/release event can be read using I2C. + GPIOPin *i2c_trigger_pin_; + + // The ISR that is used for handling event interrupts. + static void isr(FrontPanelHAL *store); + + // The number of unhandled event interrupts. + volatile int queue_length = 0; +}; + +/** + * This ISR is used to handle IRQ triggers from the front panel. + * + * The front panel pulls the trigger pin low when a new event + * is available. All we do here to handle the interrupt, is + * increment a simple queue length counter. Reading the event + * from the I2C bus will be handled in the main loop, based + * on this counter. + */ +void ICACHE_RAM_ATTR HOT FrontPanelHAL::isr(FrontPanelHAL *store) { + store->queue_length++; +} + +} // namespace bs2 +} // namespace yeelight +} // namespace esphome diff --git a/light/__init__.py b/light/__init__.py index 3e02b9f..603ede5 100644 --- a/light/__init__.py +++ b/light/__init__.py @@ -1,10 +1,12 @@ import esphome.codegen as cg import esphome.config_validation as cv -import esphome.components.gpio.output as gpio_output -from esphome.components import light, gpio, ledc -from esphome.const import CONF_RED, CONF_GREEN, CONF_BLUE, CONF_WHITE, CONF_OUTPUT_ID, CONF_TRIGGER_ID +from esphome.components import light from esphome import automation -from .. import bs2_ns, CODEOWNERS, CONF_HUB_ID, YeelightBS2Hub +from esphome.const import ( + CONF_RED, CONF_GREEN, CONF_BLUE, CONF_WHITE, + CONF_OUTPUT_ID, CONF_TRIGGER_ID +) +from .. import bs2_ns, CODEOWNERS, CONF_LIGHT_HAL_ID, LightHAL AUTO_LOAD = ["yeelight_bs2"] @@ -12,16 +14,15 @@ CONF_MASTER1 = "master1" CONF_MASTER2 = "master2" CONF_ON_BRIGHTNESS = "on_brightness" -light_state = bs2_ns.class_("YeelightBS2LightState", light.LightState) -light_output = bs2_ns.class_("YeelightBS2LightOutput", light.LightOutput) - +YeelightBS2LightState = bs2_ns.class_("YeelightBS2LightState", light.LightState) +YeelightBS2LightOutput = bs2_ns.class_("YeelightBS2LightOutput", light.LightOutput) BrightnessTrigger = bs2_ns.class_("BrightnessTrigger", automation.Trigger.template()) CONFIG_SCHEMA = light.RGB_LIGHT_SCHEMA.extend( { - cv.GenerateID(): cv.declare_id(light_state), - cv.GenerateID(CONF_HUB_ID): cv.use_id(YeelightBS2Hub), - cv.GenerateID(CONF_OUTPUT_ID): cv.declare_id(light_output), + cv.GenerateID(): cv.declare_id(YeelightBS2LightState), + cv.GenerateID(CONF_LIGHT_HAL_ID): cv.use_id(LightHAL), + cv.GenerateID(CONF_OUTPUT_ID): cv.declare_id(YeelightBS2LightOutput), cv.Optional(CONF_ON_BRIGHTNESS): automation.validate_automation( { cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(BrightnessTrigger), @@ -34,8 +35,8 @@ def to_code(config): var = cg.new_Pvariable(config[CONF_OUTPUT_ID]) yield light.register_light(var, config) - hub_var = yield cg.get_variable(config[CONF_HUB_ID]) - cg.add(var.set_hal(hub_var)) + light_hal_var = yield cg.get_variable(config[CONF_LIGHT_HAL_ID]) + cg.add(var.set_light_hal(light_hal_var)) for conf in config.get(CONF_ON_BRIGHTNESS, []): trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) diff --git a/light/light_output.h b/light/light_output.h index b044cdf..4385a91 100644 --- a/light/light_output.h +++ b/light/light_output.h @@ -1,7 +1,7 @@ #pragma once #include "../common.h" -#include "../yeelight_bs2_hub.h" +#include "../light_hal.h" #include "color_instant_handler.h" #include "color_transition_handler.h" #include "esphome/components/ledc/ledc_output.h" @@ -20,7 +20,7 @@ namespace bs2 { */ class YeelightBS2LightOutput : public Component, public light::LightOutput { public: - void set_hal(LightHAL *hal) { hal_ = hal; } + void set_light_hal(LightHAL *light) { light_ = light; } /** * Returns a LightTraits object, which is used to explain to the outside @@ -68,10 +68,10 @@ public: // 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) - hal_->light_turn_on(); + light_->turn_on(); // Apply the current GPIO output levels from the selected handler. - hal_->light_set_rgbw( + light_->set_rgbw( delegate->red, delegate->green, delegate->blue, @@ -79,13 +79,13 @@ public: ); if (values.get_state() == 0) - hal_->light_turn_off(); + light_->turn_off(); this->state_callback_.call(values); } protected: - LightHAL *hal_; + LightHAL *light_; GPIOOutputs *transition_handler_; GPIOOutputs *instant_handler_ = new ColorInstantHandler(); CallbackManager state_callback_{}; diff --git a/light_hal.h b/light_hal.h new file mode 100644 index 0000000..c86d46c --- /dev/null +++ b/light_hal.h @@ -0,0 +1,48 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/ledc/ledc_output.h" +#include "esphome/components/gpio/output/gpio_binary_output.h" + +namespace esphome { +namespace yeelight { +namespace bs2 { + +class LightHAL : Component { +public: + void set_red_pin(ledc::LEDCOutput *pin) { red_ = pin; } + void set_green_pin(ledc::LEDCOutput *pin) { green_ = pin; } + void set_blue_pin(ledc::LEDCOutput *pin) { blue_ = pin; } + void set_white_pin(ledc::LEDCOutput *pin) { white_ = pin; } + void set_master1_pin(gpio::GPIOBinaryOutput *pin) { master1_ = pin; } + void set_master2_pin(gpio::GPIOBinaryOutput *pin) { master2_ = pin; } + + void turn_on() { + master1_->turn_on(); + master2_->turn_on(); + } + + void turn_off() { + master1_->turn_off(); + master2_->turn_off(); + } + + void set_rgbw(float r, float g, float b, float w) { + red_->set_level(r); + green_->set_level(g); + blue_->set_level(b); + white_->set_level(w); + } + +protected: + ledc::LEDCOutput *red_; + ledc::LEDCOutput *green_; + ledc::LEDCOutput *blue_; + ledc::LEDCOutput *white_; + gpio::GPIOBinaryOutput *master1_; + gpio::GPIOBinaryOutput *master2_; +}; + +} // namespace bs2 +} // namespace yeelight +} // namespace esphome diff --git a/yeelight_bs2_hub.h b/yeelight_bs2_hub.h deleted file mode 100644 index 400d89c..0000000 --- a/yeelight_bs2_hub.h +++ /dev/null @@ -1,113 +0,0 @@ -#pragma once - -#include "common.h" -#include "esphome/core/component.h" -#include "esphome/core/esphal.h" -#include "esphome/components/ledc/ledc_output.h" -#include "esphome/components/gpio/output/gpio_binary_output.h" -#include "esphome/components/i2c/i2c.h" - -namespace esphome { -namespace yeelight { -namespace bs2 { - -struct TriggerPinStore { - volatile int queue_length = 0; - static void isr(TriggerPinStore *store); -}; - -/** - * This ISR is used to handle IRQ triggers from the front panel. - * - * The front panel pulls the trigger pin low when a new event - * is available. All we do here to handle the interrupt, is - * increment a simple queue length counter. Reading the event - * from the I2C bus will be handled in the main loop, based - * on this counter. - */ -void ICACHE_RAM_ATTR HOT TriggerPinStore::isr(TriggerPinStore *store) { - store->queue_length++; -} - -class LightHAL { -public: - virtual void light_turn_on() = 0; - virtual void light_turn_off() = 0; - virtual void light_set_rgbw(float r, float g, float b, float w) = 0; -}; - -class YeelightBS2Hub : public Component, public LightHAL { -public: - void set_trigger_pin(GPIOPin *pin) { i2c_trigger_pin_ = pin; } - void set_red_pin(ledc::LEDCOutput *pin) { red_ = pin; } - void set_green_pin(ledc::LEDCOutput *pin) { green_ = pin; } - void set_blue_pin(ledc::LEDCOutput *pin) { blue_ = pin; } - void set_white_pin(ledc::LEDCOutput *pin) { white_ = pin; } - void set_master1_pin(gpio::GPIOBinaryOutput *pin) { master1_ = pin; } - void set_master2_pin(gpio::GPIOBinaryOutput *pin) { master2_ = pin; } - void set_front_panel_i2c(i2c::I2CComponent *fp_i2c) { fp_i2c_ = fp_i2c; } - - void setup() { - ESP_LOGCONFIG(TAG, "Setting up I2C trigger pin interrupt..."); - this->i2c_trigger_pin_->setup(); - this->i2c_trigger_pin_->attach_interrupt( - TriggerPinStore::isr, &this->trigger_pin_store_, FALLING); - } - - void dump_config() { - ESP_LOGCONFIG(TAG, "I2C"); - LOG_PIN(" Interrupt pin: ", this->i2c_trigger_pin_); - } - - void loop() { - if (this->trigger_pin_store_.queue_length > 0) { - this->counter_++; - ESP_LOGD(TAG, "Front Panel interrupt queue=%d, counter=%d", - this->trigger_pin_store_.queue_length, this->counter_); - this->trigger_pin_store_.queue_length--; - } - } - - void light_turn_on() { - master1_->turn_on(); - master2_->turn_on(); - } - - void light_turn_off() { - master1_->turn_off(); - master2_->turn_off(); - } - - void light_set_rgbw(float r, float g, float b, float w) { - red_->set_level(r); - green_->set_level(g); - blue_->set_level(b); - white_->set_level(w); - } - -protected: - // Pins that are used for the RGBWW LEDs. - ledc::LEDCOutput *red_; - ledc::LEDCOutput *green_; - ledc::LEDCOutput *blue_; - ledc::LEDCOutput *white_; - gpio::GPIOBinaryOutput *master1_; - gpio::GPIOBinaryOutput *master2_; - - // Pin that is used by the front panel to notify the ESP that - // a touch/release event can be read using I2C. - GPIOPin *i2c_trigger_pin_; - - // The I2C bus that is connected to the front panel. - i2c::I2CComponent *fp_i2c_; - - // Fields that are used for trigger pin interrupt handling. - int counter_ = 0; - TriggerPinStore trigger_pin_store_{}; - - friend class LightHAL; -}; - -} // namespace bs2 -} // namespace yeelight -} // namespace esphome