From 6d83f80718f8a4b38eb2eee95a2afdc55aac2b46 Mon Sep 17 00:00:00 2001 From: Maurice Makaay Date: Fri, 15 Oct 2021 02:02:02 +0200 Subject: [PATCH] Compatility 2021.10.0 (#57) Made the lamp work with ESPHome 2021.10.0 --- components/xiaomi_bslamp2/__init__.py | 118 +++++---------- .../xiaomi_bslamp2/binary_sensor/__init__.py | 2 +- components/xiaomi_bslamp2/front_panel_hal.h | 95 +++++++----- components/xiaomi_bslamp2/light/__init__.py | 44 +++--- .../xiaomi_bslamp2/light/color_handler_rgb.h | 7 +- components/xiaomi_bslamp2/light/light_state.h | 2 +- components/xiaomi_bslamp2/output/__init__.py | 7 +- components/xiaomi_bslamp2/sensor/__init__.py | 2 +- .../xiaomi_bslamp2/sensor/slider_sensor.h | 3 +- example.yaml | 140 +++++++++++------- packages/core.yaml | 88 +++++++++++ 11 files changed, 302 insertions(+), 206 deletions(-) create mode 100644 packages/core.yaml diff --git a/components/xiaomi_bslamp2/__init__.py b/components/xiaomi_bslamp2/__init__.py index fcf4877..c479b2b 100644 --- a/components/xiaomi_bslamp2/__init__.py +++ b/components/xiaomi_bslamp2/__init__.py @@ -3,31 +3,19 @@ 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_LIGHT, CONF_RED, CONF_GREEN, CONF_BLUE, CONF_WHITE, + CONF_I2C, CONF_ADDRESS, CONF_TRIGGER_PIN, CONF_ID ) 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_FRONT_PANEL = "front_panel" 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") @@ -54,86 +42,48 @@ 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_LIGHT): cv.Schema( + { + cv.GenerateID(CONF_LIGHT_HAL_ID): cv.declare_id(LightHAL), + 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_FRONT_PANEL): cv.Schema( + { + cv.GenerateID(CONF_FRONT_PANEL_HAL_ID): cv.declare_id(FrontPanelHAL), + cv.Required(CONF_I2C): cv.use_id(I2CBus), + cv.Required(CONF_ADDRESS): cv.i2c_address, + cv.Required(CONF_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]) + light_hal = cg.new_Pvariable(config[CONF_LIGHT][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_LIGHT][CONF_RED]))) + cg.add(light_hal.set_green_pin(await cg.get_variable(config[CONF_LIGHT][CONF_GREEN]))) + cg.add(light_hal.set_blue_pin(await cg.get_variable(config[CONF_LIGHT][CONF_BLUE]))) + cg.add(light_hal.set_white_pin(await cg.get_variable(config[CONF_LIGHT][CONF_WHITE]))) + cg.add(light_hal.set_master1_pin(await cg.get_variable(config[CONF_LIGHT][CONF_MASTER1]))) + cg.add(light_hal.set_master2_pin(await cg.get_variable(config[CONF_LIGHT][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]) + fp_hal = cg.new_Pvariable(config[CONF_FRONT_PANEL][CONF_FRONT_PANEL_HAL_ID]) await cg.register_component(fp_hal, config) + trigger_pin = await cg.gpio_pin_expression(config[CONF_FRONT_PANEL][CONF_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_FRONT_PANEL][CONF_I2C]) + cg.add(fp_hal.set_i2c_bus(fp_i2c_var)) + cg.add(fp_hal.set_i2c_address(config[CONF_FRONT_PANEL][CONF_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/binary_sensor/__init__.py b/components/xiaomi_bslamp2/binary_sensor/__init__.py index 839f13a..2125bab 100644 --- a/components/xiaomi_bslamp2/binary_sensor/__init__.py +++ b/components/xiaomi_bslamp2/binary_sensor/__init__.py @@ -8,7 +8,7 @@ from .. import ( CONF_FRONT_PANEL_HAL_ID, FrontPanelHAL ) -AUTO_LOAD = ["xiaomi_bslamp2"] +DEPENDENCIES = ["xiaomi_bslamp2"] CONF_PART = "part" diff --git a/components/xiaomi_bslamp2/front_panel_hal.h b/components/xiaomi_bslamp2/front_panel_hal.h index cbe524b..af92872 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,23 @@ class FrontPanelEventParser { } }; +struct FrontPanelTriggerStore { + 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 +211,52 @@ 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->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 +300,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 +330,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 +340,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/__init__.py b/components/xiaomi_bslamp2/light/__init__.py index d57e6d6..a1ba68f 100644 --- a/components/xiaomi_bslamp2/light/__init__.py +++ b/components/xiaomi_bslamp2/light/__init__.py @@ -2,7 +2,7 @@ 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, Lambda +from esphome.core import Lambda from esphome.const import ( CONF_RED, CONF_GREEN, CONF_BLUE, CONF_WHITE, CONF_COLOR_TEMPERATURE, CONF_STATE, CONF_OUTPUT_ID, CONF_TRIGGER_ID, CONF_ID, @@ -10,17 +10,15 @@ from esphome.const import ( ) from .. import bslamp2_ns, CODEOWNERS, CONF_LIGHT_HAL_ID, LightHAL -AUTO_LOAD = ["xiaomi_bslamp2"] +DEPENDENCIES = ["xiaomi_bslamp2"] -CONF_MASTER1 = "master1" -CONF_MASTER2 = "master2" 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_PRESET = "preset" MIRED_MIN = 153 MIRED_MAX = 588 @@ -219,23 +217,20 @@ def preset_activate_to_code(config, action_id, template_arg, args): cg.add(action_var.set_group(group_template_)) yield action_var -@coroutine -def light_output_to_code(config): +async 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]) + 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)) -@coroutine -def on_brightness_to_code(config): - light_output_var = yield cg.get_variable(config[CONF_OUTPUT_ID]) +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) - yield automation.build_automation(trigger, [(float, "x")], config) + await automation.build_automation(trigger, [(float, "x")], config) -@coroutine -def preset_to_code(config, preset_group, preset_name): - light_var = yield cg.get_variable(config[CONF_ID]) +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: @@ -254,22 +249,21 @@ def preset_to_code(config, preset_group, preset_name): cg.add(preset_var.set_effect(config[CONF_EFFECT])) else: cg.add(preset_var.set_effect("None")) - yield cg.register_component(preset_var, config) + return await cg.register_component(preset_var, config) -@coroutine -def presets_to_code(config): +async def presets_to_code(config): presets_var = cg.new_Pvariable(config[CONF_PRESETS_ID]) - yield cg.register_component(presets_var, config) + 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 = yield preset_to_code(preset_config, preset_group, preset_name) + preset = await 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) +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, {}); 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/light/light_state.h b/components/xiaomi_bslamp2/light/light_state.h index 1f18cd3..10642e8 100644 --- a/components/xiaomi_bslamp2/light/light_state.h +++ b/components/xiaomi_bslamp2/light/light_state.h @@ -29,7 +29,7 @@ struct MyLightStateRTCState { */ class XiaomiBslamp2LightState : public light::LightState, public LightStateDiscoSupport { public: - XiaomiBslamp2LightState(const std::string &name, XiaomiBslamp2LightOutput *output) : light::LightState(name, output) { } + XiaomiBslamp2LightState(XiaomiBslamp2LightOutput *output) : light::LightState(output) { } void disco_stop() { MyLightStateRTCState recovered{}; diff --git a/components/xiaomi_bslamp2/output/__init__.py b/components/xiaomi_bslamp2/output/__init__.py index 0e13e9e..3f51a53 100644 --- a/components/xiaomi_bslamp2/output/__init__.py +++ b/components/xiaomi_bslamp2/output/__init__.py @@ -5,11 +5,12 @@ from esphome.const import CONF_ID, CONF_LEVEL from esphome import automation from .. import ( bslamp2_ns, CODEOWNERS, - CONF_FRONT_PANEL_HAL_ID, FrontPanelHAL, FRONT_PANEL_LED_OPTIONS, - CONF_LEDS + CONF_FRONT_PANEL_HAL_ID, FrontPanelHAL, FRONT_PANEL_LED_OPTIONS ) -AUTO_LOAD = ["xiaomi_bslamp2"] +CONF_LEDS = "leds" + +DEPENDENCIES = ["xiaomi_bslamp2"] XiaomiBslamp2FrontPanelOutput = bslamp2_ns.class_( "XiaomiBslamp2FrontPanelOutput", output.FloatOutput, cg.Component) diff --git a/components/xiaomi_bslamp2/sensor/__init__.py b/components/xiaomi_bslamp2/sensor/__init__.py index 039ae65..fd95a29 100644 --- a/components/xiaomi_bslamp2/sensor/__init__.py +++ b/components/xiaomi_bslamp2/sensor/__init__.py @@ -7,7 +7,7 @@ from .. import ( CONF_FRONT_PANEL_HAL_ID, FrontPanelHAL ) -AUTO_LOAD = ["xiaomi_bslamp2"] +DEPENDENCIES = ["xiaomi_bslamp2"] XiaomiBslamp2SliderSensor = bslamp2_ns.class_( "XiaomiBslamp2SliderSensor", sensor.Sensor, cg.Component) 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); diff --git a/example.yaml b/example.yaml index 6660eb7..47e4995 100644 --- a/example.yaml +++ b/example.yaml @@ -7,32 +7,25 @@ substitutions: friendly_name: Bedside Lamp transition_length: 500ms - # Component identifiers. - prefix: bedside_lamp - id_light: ${prefix} - id_light_mode: ${prefix}_light_mode - id_power_button: ${prefix}_power_button - id_color_button: ${prefix}_color_button - id_slider_level: ${prefix}_slider_level - id_front_panel_illumination: ${prefix}_front_panel_illumination - # -------------------------------------------------------------------------- # Use your own preferences for these components. # -------------------------------------------------------------------------- +# The log level can be raised when needed for debugging the firmware. For +# production, a low log level is recommended. Mainly because high volume log +# output might interfere with the API/WiFi connection stability. So when +# raising the log level, beware that you might see dropped connections from +# Home Assistant and the network log viewer. +logger: + level: WARN + wifi: ssid: "Your-SSID" password: "Your-WiFi-Network-Password" - # Enable fallback hotspot (for captive portal) in case wifi connection fails - ap: - ssid: "ESPHome $friendly_name" - password: "Password-For-Connecting-To-Captive-Portal" - -captive_portal: - api: - password: "Password-To-Link-HomeAssistant-To-This-Device" + password: "Password-For-Linking-HomeAssistant-To-This-Device" + # Disable the reboot timeout. By default, the lamp reboots after 15 # minutes without any client connections (e.g. when home assistant is off # line, or when the WiFi is broken). Reboots are annoying though, because @@ -40,7 +33,7 @@ api: # flicker. reboot_timeout: 0s - # If you want to use light presets (see below) from Home Assistant, + # If you want to control light presets (see below) from Home Assistant, # then you can expose the required functionality as a service here. # This is an example of how you could expose the activation of a preset. services: @@ -65,7 +58,7 @@ ota: on_begin: then: - light.disco_on: - id: ${id_light} + id: my_light red: 0% green: 0% blue: 100% @@ -78,7 +71,7 @@ ota: on_end: then: - light.disco_on: - id: ${id_light} + id: my_light red: 0% green: 100% blue: 0% @@ -87,28 +80,22 @@ ota: on_error: then: - light.disco_on: - id: ${id_light} + id: my_light red: 100% green: 0% blue: 0% brightness: 2% - delay: 1s - light.disco_off: - id: ${id_light} - -# The log level can be raised when needed for debugging the firmware. For -# production, a low log level is recommended. Mainly because high volume log -# output might interfere with the API/WiFi connection stability. So when -# raising the log level, beware that you might see dropped connections from -# Home Assistant and the network log viewer. -logger: - level: WARN + id: my_light # -------------------------------------------------------------------------- # Configuration specific for the Xiaomi Mijia Bedside Lamp 2. -# This is just an example. You can of course modify it for your own needs. # -------------------------------------------------------------------------- +esphome: + name: ${name} + # Retrieve the code for the xiaomi_bslamp2 platform from GitHub. external_components: - source: @@ -120,19 +107,72 @@ external_components: # A special platform package is used for enabling unicore and disabling the # efuse mac crc check. These two changes are required for the ESP32-WROOM-32D # chip that is used in the lamp. -esphome: - name: ${name} - platform: ESP32 +esp32: board: esp32doit-devkit-v1 - platformio_options: - platform: espressif32@3.2.0 - platform_packages: |- - framework-arduinoespressif32 @ https://github.com/mmakaay/arduino-esp32-unicore-no-mac-crc#v1.0.6 + framework: + type: arduino + platform_version: 3.3.2 + version: https://github.com/mmakaay/arduino-esp32-unicore-no-mac-crc#v1.0.6 + version_hint: 1.0.6 + +# The I2C bus that is used for communicating to the front panel. +i2c: + id: front_panel_i2c + sda: GPIO21 + scl: GPIO19 + scan: true + +# Outputs for driving the LEDs of the lamp. +output: + - platform: ledc + id: output_red + pin: GPIO13 + frequency: 3000 + - platform: ledc + id: output_green + pin: GPIO14 + frequency: 3000 + - platform: ledc + id: output_blue + pin: GPIO5 + frequency: 3000 + - platform: ledc + id: output_white + pin: GPIO12 + frequency: 10000 + - platform: gpio + id: output_master1 + pin: GPIO33 + - platform: gpio + id: output_master2 + pin: GPIO4 + mode: OUTPUT + +# The main configuration for the lamp. This sets up the two hardware +# abstraction layers for the light and the front panel. These are +# used by the other components. +xiaomi_bslamp2: + light: + red: output_red + green: output_green + blue: output_blue + white: output_white + master1: output_master1 + master2: output_master2 + front_panel: + i2c: front_panel_i2c + address: 0x2C + trigger_pin: GPIO16 + +# -------------------------------------------------------------------------- +# Configuration of the behaviors for the lamp. +# This is just an example. You can of course modify it for your own needs. +# -------------------------------------------------------------------------- # This component controls the LED lights of the lamp. light: - platform: xiaomi_bslamp2 - id: ${id_light} + id: my_light name: ${friendly_name} RGBWW Light default_transition_length: ${transition_length} # When the brightness is changed, then update the level indicator @@ -142,15 +182,15 @@ light: - if: condition: text_sensor.state: - id: ${id_light_mode} + id: my_light_mode state: night then: - output.set_level: - id: ${id_front_panel_illumination} + id: my_front_panel_illumination level: 0 else: - output.set_level: - id: ${id_front_panel_illumination} + id: my_front_panel_illumination level: !lambda return x; # You can use any effects that you like. These are just examples. effects: @@ -190,14 +230,14 @@ light: text_sensor: - platform: xiaomi_bslamp2 name: ${friendly_name} Light Mode - id: ${id_light_mode} + id: my_light_mode # This float output controls the front panel illumination + level indicator. # Value 0.0 turns off the illumination. Other values (up to 1.0) turn on # the illumination and set the level indicator to the requested level. output: - platform: xiaomi_bslamp2 - id: ${id_front_panel_illumination} + id: my_front_panel_illumination # Binary sensors can be created for handling front panel touch / release # events. To specify what part of the front panel to look at, the "for" @@ -206,24 +246,24 @@ binary_sensor: # When tapping the power button, toggle the light. # When holding the power button, turn on night light mode. - platform: xiaomi_bslamp2 - id: ${id_power_button} + id: my_power_button for: POWER_BUTTON on_multi_click: - timing: - ON for at most 0.8s then: - - light.toggle: ${id_light} + - light.toggle: my_light - timing: - ON for at least 0.8s then: - light.turn_on: - id: ${id_light} + id: my_light brightness: 1% # When tapping the color button, acivate the next preset. # When holding the color button, activate the next preset group. - platform: xiaomi_bslamp2 - id: ${id_color_button} + id: my_color_button for: COLOR_BUTTON on_multi_click: - timing: @@ -247,11 +287,11 @@ sensor: # been handled above (by holding the power button). Therefore, brightness # starts from 0.02 here, to not trigger night mode using the slider. - platform: xiaomi_bslamp2 - id: ${id_slider_level} + id: my_slider_level range_from: 0.02 on_value: then: - light.turn_on: - id: ${id_light} + id: my_light brightness: !lambda return x; diff --git a/packages/core.yaml b/packages/core.yaml new file mode 100644 index 0000000..a62f14e --- /dev/null +++ b/packages/core.yaml @@ -0,0 +1,88 @@ +# -------------------------------------------------------------------------- +# Xiaomi Mija Bedside Lamp 2 core configuration +# -------------------------------------------------------------------------- + +substitutions: + name: bedside-lamp + friendly_name: Bedside Lamp + light_name: ${friendly_name} RGBWW Light + light_mode_text_sensor_name: ${friendly_name} Light Mode + default_transition_length: 800ms + +esphome: + name: ${name} + +esp32: + board: esp32doit-devkit-v1 + framework: + type: esp-idf + sdkconfig_options: + CONFIG_FREERTOS_UNICORE: y + advanced: + ignore_efuse_mac_crc: true + +# Retrieve the code for the xiaomi_bslamp2 platform from GitHub. +external_components: + - source: github://mmakaay/esphome-xiaomi_bslamp2@2021.10.0 + refresh: 60 + +# Disable the reboot timeout. By default, the lamp reboots after 15 +# minutes without any client connections (e.g. when home assistant is off +# line, or when the WiFi is broken). Reboots are annoying though, because +# the RGBWW LEDs will turn off during the reboot, causing the light to +# flicker. +api: + reboot_timeout: 0s + +# ---------------------------------------------------------------------- +# Hardware configuration +# ---------------------------------------------------------------------- + +i2c: + id: front_panel_i2c + sda: GPIO21 + scl: GPIO19 + scan: true + +output: + - platform: ledc + id: output_red + pin: GPIO13 + frequency: 3000 + channel: 0 + - platform: ledc + id: output_green + pin: GPIO14 + frequency: 3000 + channel: 1 + - platform: ledc + id: output_blue + pin: GPIO5 + frequency: 3000 + channel: 2 + - platform: ledc + id: output_white + pin: GPIO12 + frequency: 10000 + channel: 4 + - platform: gpio + id: output_master1 + pin: GPIO33 + - platform: gpio + id: output_master2 + pin: + number: GPIO4 + mode: OUTPUT + +xiaomi_bslamp2: + light: + red: output_red + green: output_green + blue: output_blue + white: output_white + master1: output_master1 + master2: output_master2 + front_panel: + i2c: front_panel_i2c + address: 0x2C + trigger_pin: GPIO16