diff --git a/components/xiaomi_bslamp2/__init__.py b/components/xiaomi_bslamp2/__init__.py index fcf4877..d62bf47 100644 --- a/components/xiaomi_bslamp2/__init__.py +++ b/components/xiaomi_bslamp2/__init__.py @@ -3,32 +3,26 @@ import esphome.config_validation as cv from esphome import pins from esphome.components.ledc.output import LEDCOutput from esphome.components.gpio.output import GPIOBinaryOutput -from esphome.components.i2c import I2CComponent, I2CDevice -from esphome.core import coroutine -from esphome.core import CORE +from esphome.components.i2c import I2CBus, I2CDevice from esphome.const import ( - CONF_RED, CONF_GREEN, CONF_BLUE, CONF_WHITE, CONF_TRIGGER_PIN, - CONF_SDA, CONF_SCL, CONF_ADDRESS, CONF_PLATFORM + CONF_RED, CONF_GREEN, CONF_BLUE, CONF_WHITE, + CONF_TRIGGER_PIN, CONF_SDA, CONF_SCL, CONF_ADDRESS ) +# TODO subsection in config for leds and front_panel. + CODEOWNERS = ["@mmakaay"] -CONF_RED_ID = "red_id" -CONF_GREEN_ID = "green_id" -CONF_BLUE_ID = "blue_id" -CONF_WHITE_ID = "white_id" CONF_MASTER1 = "master1" -CONF_MASTER1_ID = "master1_id" CONF_MASTER2 = "master2" -CONF_MASTER2_ID = "master2_id" -CONF_FP_I2C_ID = "front_panel_i2c_id" +CONF_FP_I2C = "fp_i2c" +CONF_FP_I2C_ADDRESS = "fp_i2c_address" +CONF_FP_TRIGGER_PIN = "fp_trigger_pin" CONF_LIGHT_HAL_ID = "light_hal_id" CONF_FRONT_PANEL_HAL_ID = "front_panel_hal_id" CONF_ON_BRIGHTNESS = "on_brightness" CONF_LEDS = "leds" -AUTO_LOAD = ["ledc", "output", "i2c"] - xiaomi_ns = cg.esphome_ns.namespace("xiaomi") bslamp2_ns = xiaomi_ns.namespace("bslamp2") LightHAL = bslamp2_ns.class_("LightHAL", cg.Component) @@ -55,85 +49,40 @@ FRONT_PANEL_LED_OPTIONS = { CONFIG_SCHEMA = cv.COMPONENT_SCHEMA.extend({ # 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, + cv.Required(CONF_RED): cv.use_id(LEDCOutput), + cv.Required(CONF_GREEN): cv.use_id(LEDCOutput), + cv.Required(CONF_BLUE): cv.use_id(LEDCOutput), + cv.Required(CONF_WHITE): cv.use_id(LEDCOutput), + cv.Required(CONF_MASTER1): cv.use_id(GPIOBinaryOutput), + cv.Required(CONF_MASTER2): cv.use_id(GPIOBinaryOutput), # 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="GPIO21"): pins.validate_gpio_pin, - cv.Optional(CONF_SCL, default="GPIO19"): pins.validate_gpio_pin, - cv.Optional(CONF_ADDRESS, default="0x2C"): cv.i2c_address, - cv.Optional(CONF_TRIGGER_PIN, default="GPIO16"): cv.All( - pins.validate_gpio_pin, - pins.validate_has_interrupt - ), + cv.Required(CONF_FP_I2C): cv.use_id(I2CBus), + cv.Required(CONF_FP_I2C_ADDRESS): cv.i2c_address, + cv.Required(CONF_FP_TRIGGER_PIN): cv.All(pins.internal_gpio_input_pin_schema) }) -async def make_gpio(number, mode="OUTPUT"): - return await cg.gpio_pin_expression({ "number": number, "mode": mode }); - -async def make_gpio_binary_output(id_, number): - gpio_var = await make_gpio(number) - output_var = cg.new_Pvariable(id_) - cg.add(output_var.set_pin(gpio_var)) - return await cg.register_component(output_var, {}) - -async def make_ledc_output(id_, number, frequency, channel): - gpio_var = await 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)); - return await cg.register_component(ledc_var, {}) - async def make_light_hal(config): - r_var = await make_ledc_output(config[CONF_RED_ID], config[CONF_RED], 3000, 0) - g_var = await make_ledc_output(config[CONF_GREEN_ID], config[CONF_GREEN], 3000, 1) - b_var = await make_ledc_output(config[CONF_BLUE_ID], config[CONF_BLUE], 3000, 2) - w_var = await make_ledc_output(config[CONF_WHITE_ID], config[CONF_WHITE], 10000, 4) - m1_var = await make_gpio_binary_output(config[CONF_MASTER1_ID], config[CONF_MASTER1]) - m2_var = await make_gpio_binary_output(config[CONF_MASTER2_ID], config[CONF_MASTER2]) light_hal = cg.new_Pvariable(config[CONF_LIGHT_HAL_ID]) await 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)) + cg.add(light_hal.set_red_pin(await cg.get_variable(config[CONF_RED]))) + cg.add(light_hal.set_green_pin(await cg.get_variable(config[CONF_GREEN]))) + cg.add(light_hal.set_blue_pin(await cg.get_variable(config[CONF_BLUE]))) + cg.add(light_hal.set_white_pin(await cg.get_variable(config[CONF_WHITE]))) + cg.add(light_hal.set_master1_pin(await cg.get_variable(config[CONF_MASTER1]))) + cg.add(light_hal.set_master2_pin(await cg.get_variable(config[CONF_MASTER2]))) async def make_front_panel_hal(config): - trigger_pin = await make_gpio(config[CONF_TRIGGER_PIN], "INPUT") fp_hal = cg.new_Pvariable(config[CONF_FRONT_PANEL_HAL_ID]) await cg.register_component(fp_hal, config) + trigger_pin = await cg.gpio_pin_expression(config[CONF_FP_TRIGGER_PIN]) 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 - # front panel I2C communication. - fp_i2c_var = await cg.get_variable(config[CONF_FP_I2C_ID]) - 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(fp_hal.set_i2c_parent(fp_i2c_var)) - cg.add(fp_hal.set_i2c_address(config[CONF_ADDRESS])) + fp_i2c_var = await cg.get_variable(config[CONF_FP_I2C]) + cg.add(fp_hal.set_i2c_bus(fp_i2c_var)) + cg.add(fp_hal.set_i2c_address(config[CONF_FP_I2C_ADDRESS])) async def to_code(config): - # Dirty little hack to make the ESPHome component loader include - # 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" }) - await make_light_hal(config) await make_front_panel_hal(config) diff --git a/components/xiaomi_bslamp2/front_panel_hal.h b/components/xiaomi_bslamp2/front_panel_hal.h index cbe524b..8e385ac 100644 --- a/components/xiaomi_bslamp2/front_panel_hal.h +++ b/components/xiaomi_bslamp2/front_panel_hal.h @@ -3,7 +3,7 @@ #include "common.h" #include "esphome/components/i2c/i2c.h" #include "esphome/core/component.h" -#include "esphome/core/esphal.h" +#include "esphome/core/log.h" #include #include @@ -100,7 +100,7 @@ class FrontPanelEventParser { // All events use the prefix [04:04:01:00]. if (m[0] != 0x04 || m[1] != 0x04 || m[2] != 0x01 || m[3] != 0x00) { - return error_(ev, m, "prefix is not 04:04:01:00"); + return this->error_(ev, m, "prefix is not 04:04:01:00"); } // The next byte determines the part that is touched. @@ -114,23 +114,23 @@ class FrontPanelEventParser { else if (m[5] == 0x02 && m[6] == (0x03 + m[4])) ev |= FLAG_TYPE_RELEASE; else - return error_(ev, m, "invalid event type for button"); + return this->error_(ev, m, "invalid event type for button"); break; case 0x03: // slider touch case 0x04: // slider release ev |= FLAG_PART_SLIDER; ev |= (m[4] == 0x03 ? FLAG_TYPE_TOUCH : FLAG_TYPE_RELEASE); if ((m[6] - m[5] - m[4] - 0x01) != 0) - return error_(ev, m, "invalid slider level crc"); + return this->error_(ev, m, "invalid slider level crc"); else if (m[5] > 0x16 || m[5] < 0x01) - return error_(ev, m, "out of bounds slider value"); + return this->error_(ev, m, "out of bounds slider value"); else { auto level = 0x17 - m[5]; ev |= (level << FLAG_LEVEL_SHIFT); } break; default: - return error_(ev, m, "invalid part id"); + return this->error_(ev, m, "invalid part id"); return ev; } @@ -148,8 +148,8 @@ class FrontPanelEventParser { ESP_LOGE(TAG, "Front panel I2C event error:"); ESP_LOGE(TAG, " Error: %s", msg); ESP_LOGE(TAG, " Event: [%02x:%02x:%02x:%02x:%02x:%02x:%02x]", m[0], m[1], m[2], m[3], m[4], m[5], m[6]); - ESP_LOGE(TAG, " Parsed part: %s", format_part(ev)); - ESP_LOGE(TAG, " Parsed event type: %s", format_event_type(ev)); + ESP_LOGE(TAG, " Parsed part: %s", this->format_part_(ev)); + ESP_LOGE(TAG, " Parsed event type: %s", this->format_event_type_(ev)); if (has_(ev, FLAG_PART_MASK, FLAG_PART_SLIDER)) { auto level = (ev & FLAG_LEVEL_MASK) >> FLAG_LEVEL_SHIFT; if (level > 0) { @@ -160,7 +160,7 @@ class FrontPanelEventParser { return ev; } - const char *format_part(EVENT ev) { + const char *format_part_(EVENT ev) { if (has_(ev, FLAG_PART_MASK, FLAG_PART_POWER)) return "power button"; if (has_(ev, FLAG_PART_MASK, FLAG_PART_COLOR)) @@ -170,7 +170,7 @@ class FrontPanelEventParser { return "n/a"; } - const char *format_event_type(EVENT ev) { + const char *format_event_type_(EVENT ev) { if (has_(ev, FLAG_TYPE_MASK, FLAG_TYPE_TOUCH)) return "touch"; if (has_(ev, FLAG_TYPE_MASK, FLAG_TYPE_RELEASE)) @@ -179,6 +179,24 @@ class FrontPanelEventParser { } }; +struct FrontPanelTriggerStore { + ISRInternalGPIOPin pin; + volatile int event_id{0}; + static void gpio_intr(FrontPanelTriggerStore *store); +}; + +/** + * This ISR is used to handle IRQ triggers from the front panel. + * + * The front panel pulls the trigger pin low for a short period of time + * when a new event is available. All we do here to handle the interrupt, + * is increment a simple event id counter. The main loop of the component + * will take care of actually reading and processing the event. + */ +void IRAM_ATTR HOT FrontPanelTriggerStore::gpio_intr(FrontPanelTriggerStore *store) { + store->event_id++; +} + /** * This is a hardware abstraction layer that communicates with with front * panel of the Xiaomi Mijia Bedside Lamp 2. @@ -194,37 +212,53 @@ class FrontPanelHAL : public Component, public i2c::I2CDevice { * Set the GPIO pin that is used by the front panel to notify the ESP * that a touch/release event can be read using I2C. */ - void set_trigger_pin(GPIOPin *pin) { trigger_pin_ = pin; } + void set_trigger_pin(InternalGPIOPin *pin) { + trigger_pin_ = pin; + } - void add_on_event_callback(std::function &&callback) { event_callback_.add(std::move(callback)); } + void add_on_event_callback(std::function &&callback) { + event_callback_.add(std::move(callback)); + } void setup() { ESP_LOGCONFIG(TAG, "Setting up I2C trigger pin interrupt..."); - trigger_pin_->setup(); - trigger_pin_->attach_interrupt(FrontPanelHAL::isr, this, FALLING); + this->trigger_pin_->setup(); + this->store_.pin = this->trigger_pin_->to_isr(); + this->trigger_pin_->attach_interrupt( + FrontPanelTriggerStore::gpio_intr, + &this->store_, + gpio::INTERRUPT_FALLING_EDGE); } void dump_config() { ESP_LOGCONFIG(TAG, "FrontPanelHAL:"); + LOG_I2C_DEVICE(this); LOG_PIN(" I2C interrupt pin: ", trigger_pin_); } void loop() { // Read and publish front panel events. - auto current_event_id = event_id_; - if (current_event_id != last_event_id_) { - last_event_id_ = current_event_id; + auto current_event_id = this->store_.event_id; + if (current_event_id != this->last_event_id_) { + this->last_event_id_ = current_event_id; + if (this->write(READY_FOR_EV, MSG_LEN) != i2c::ERROR_OK) { + ESP_LOGW(TAG, "Writing READY_FOR_EV to front panel failed"); + } MSG message; - if (write_bytes_raw(READY_FOR_EV, MSG_LEN) && read_bytes_raw(message, MSG_LEN)) { - auto ev = event.parse(message); - if (ev & FLAG_OK) { - event_callback_.call(ev); - } + if (this->read(message, MSG_LEN) != i2c::ERROR_OK) { + ESP_LOGW(TAG, "Reading message from front panel failed"); + return; + } + auto ev = event.parse(message); + if (ev & FLAG_OK) { + this->event_callback_.call(ev); + } else { + ESP_LOGW(TAG, "Skipping unsupported message from front panel"); } } if (led_state_ != last_led_state_) { - update_leds(); + update_leds(); } } @@ -268,7 +302,7 @@ class FrontPanelHAL : public Component, public i2c::I2CDevice { void update_leds() { led_msg_[2] = led_state_ >> 8; led_msg_[3] = led_state_ & 0xff; - write_bytes_raw(led_msg_, MSG_LEN); + write(led_msg_, MSG_LEN); last_led_state_ = led_state_; } @@ -298,9 +332,8 @@ class FrontPanelHAL : public Component, public i2c::I2CDevice { } protected: - GPIOPin *trigger_pin_; - static void isr(FrontPanelHAL *store); - volatile int event_id_ = 0; + InternalGPIOPin *trigger_pin_; + FrontPanelTriggerStore store_{}; int last_event_id_ = 0; CallbackManager event_callback_{}; @@ -309,16 +342,6 @@ class FrontPanelHAL : public Component, public i2c::I2CDevice { MSG led_msg_ = {0x02, 0x03, 0x00, 0x00, 0x64, 0x00, 0x00}; }; -/** - * This ISR is used to handle IRQ triggers from the front panel. - * - * The front panel pulls the trigger pin low for a short period of time - * when a new event is available. All we do here to handle the interrupt, - * is increment a simple event id counter. The main loop of the component - * will take care of actually reading and processing the event. - */ -void ICACHE_RAM_ATTR HOT FrontPanelHAL::isr(FrontPanelHAL *store) { store->event_id_++; } - } // namespace bslamp2 } // namespace xiaomi } // namespace esphome diff --git a/components/xiaomi_bslamp2/light/color_handler_rgb.h b/components/xiaomi_bslamp2/light/color_handler_rgb.h index 7d2896c..46a0f7e 100644 --- a/components/xiaomi_bslamp2/light/color_handler_rgb.h +++ b/components/xiaomi_bslamp2/light/color_handler_rgb.h @@ -2,6 +2,7 @@ #include #include +#include #include "../common.h" #include "../light_hal.h" @@ -259,7 +260,7 @@ class ColorHandlerRGB : public ColorHandler { // 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 = min(min(v.get_red(), v.get_green()), v.get_blue()); + 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 @@ -343,8 +344,8 @@ class ColorHandlerRGB : public ColorHandler { * Returns the position on an RGB ring in degrees (0 - 359). */ float ring_pos_(float red, float green, float blue) { - auto rgb_min = min(min(red, green), blue); - auto rgb_max = max(max(red, green), 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) diff --git a/components/xiaomi_bslamp2/sensor/slider_sensor.h b/components/xiaomi_bslamp2/sensor/slider_sensor.h index 4f4294f..1452b2f 100644 --- a/components/xiaomi_bslamp2/sensor/slider_sensor.h +++ b/components/xiaomi_bslamp2/sensor/slider_sensor.h @@ -4,6 +4,7 @@ #include "../front_panel_hal.h" #include "esphome/components/sensor/sensor.h" #include +#include namespace esphome { namespace xiaomi { @@ -37,7 +38,7 @@ class XiaomiBslamp2SliderSensor : public sensor::Sensor, public Component { // look like this one was ever meant to be used, or that // the design was faulty on this. Therefore, level 1 is // ignored. The resulting range of levels is 0-19. - float corrected_level = max(0.0f, level - 2.0f); + float corrected_level = std::max(0.0f, level - 2.0f); float final_level = range_from_ + (slope_ * corrected_level);