diff --git a/__init__.py b/__init__.py index f88e4dc..abedfa9 100644 --- a/__init__.py +++ b/__init__.py @@ -7,9 +7,8 @@ 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_RED, CONF_GREEN, CONF_BLUE, CONF_WHITE, CONF_TRIGGER_PIN, + CONF_SDA, CONF_SCL, CONF_ADDRESS, CONF_PLATFORM ) CODEOWNERS = ["@mmakaay"] @@ -58,6 +57,7 @@ def make_config_schema(): 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 @@ -119,6 +119,7 @@ def make_front_panel_hal(config): 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])) def to_code(config): # Dirty little hack to make the ESPHome component loader inlcude diff --git a/front_panel_hal.h b/front_panel_hal.h index fdd3a82..2ee51c7 100644 --- a/front_panel_hal.h +++ b/front_panel_hal.h @@ -1,5 +1,6 @@ #pragma once +#include #include "common.h" #include "esphome/core/component.h" #include "esphome/core/esphal.h" @@ -9,39 +10,178 @@ namespace esphome { namespace yeelight { namespace bs2 { +static const uint8_t MSG_LEN = 7; +using MSG = uint8_t[7]; +static const MSG READY_FOR_EV = { 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01 }; +static const MSG TURN_ON = { 0x02, 0x03, 0x5E, 0x00, 0x64, 0x00, 0x00 }; +static const MSG TURN_OFF = { 0x02, 0x03, 0x0C, 0x00, 0x64, 0x00, 0x00 }; +static const MSG SET_LEVEL_1 = { 0x02, 0x03, 0x5E, 0x00, 0x64, 0x00, 0x00 }; +static const MSG SET_LEVEL_2 = { 0x02, 0x03, 0x5F, 0x00, 0x64, 0x00, 0x00 }; +static const MSG SET_LEVEL_3 = { 0x02, 0x03, 0x5F, 0x80, 0x64, 0x00, 0x00 }; +static const MSG SET_LEVEL_4 = { 0x02, 0x03, 0x5F, 0xC0, 0x64, 0x00, 0x00 }; +static const MSG SET_LEVEL_5 = { 0x02, 0x03, 0x5F, 0xE0, 0x64, 0x00, 0x00 }; +static const MSG SET_LEVEL_6 = { 0x02, 0x03, 0x5F, 0xF0, 0x64, 0x00, 0x00 }; +static const MSG SET_LEVEL_7 = { 0x02, 0x03, 0x5F, 0xF8, 0x64, 0x00, 0x00 }; +static const MSG SET_LEVEL_8 = { 0x02, 0x03, 0x5F, 0xFC, 0x64, 0x00, 0x00 }; +static const MSG SET_LEVEL_9 = { 0x02, 0x03, 0x5F, 0xFE, 0x64, 0x00, 0x00 }; +static const MSG SET_LEVEL_10 = { 0x02, 0x03, 0x5F, 0xFF, 0x64, 0x00, 0x00 }; + +enum FrontPanelButton { + ButtonUnknown, + ButtonPower, + ButtonColor, + ButtonSlider +}; + +enum FrontPanelEventType { + TypeUnknown, + TypeTouch, + TypeRelease +}; + +class FrontPanelEvent { +public: + bool valid; + FrontPanelEventType type; + FrontPanelButton button; + uint8_t level; + MSG message; + + void parse(uint8_t *m) { + memcpy(message, m, MSG_LEN); + type = TypeUnknown; + button = ButtonUnknown; + valid = true; + level = 0; + + // All events start with 04:04:01:00. + if (m[0] != 0x04 || m[1] != 0x04 || m[2] != 0x01 || m[3] != 0x00) { + valid = false; + return; + } + + // Next byte determines the button that is touched. + // All remaining bytes determine the event for that button. + switch (m[4]) { + case 0x01: + button = ButtonPower; + if (m[5] == 0x01 && m[6] == 0x03) { + type = TypeTouch; + } else if (m[5] == 0x02 && m[6] == 0x04) { + type = TypeRelease; + } else { + valid = false; + } + break; + case 0x02: + button = ButtonColor; + if (m[5] == 0x01 && m[6] == 0x04) { + type = TypeTouch; + } else if (m[5] == 0x02 && m[6] == 0x05) { + type = TypeRelease; + } else { + valid = false; + } + break; + case 0x03: + case 0x04: + button = ButtonSlider; + type = m[4] == 0x03 ? TypeTouch : TypeRelease; + if ((m[6] - m[5] - m[4] - 0x01) != 0) { + valid = false; + } else if (m[5] > 0x16 || m[5] < 0x01) { + valid = false; + } else { + level = 0x17 - m[5]; + } + break; + default: + valid = false; + return; + } + } + + void log() { + if (button == ButtonSlider) { + ESP_LOGI(TAG, "Event %0x:%0x:%0x:%0x:%0x:%0x:%0x => ok=%s, button=%s, type=%s, level=%d", + message[0], message[1], message[2], message[3], message[4], + message[5], message[6], + (valid ? "Y" : "N"), button_str_(), type_str_(), level); + } else { + ESP_LOGI(TAG, "Event %0x:%0x:%0x:%0x:%0x:%0x:%0x => ok=%s, button=%s, type=%s", + message[0], message[1], message[2], message[3], message[4], + message[5], message[6], + (valid ? "Y" : "N"), button_str_(), type_str_()); + } + } +protected: + const char *button_str_() { + switch (button) { + case ButtonPower: return "POWER"; break; + case ButtonColor: return "COLOR"; break; + case ButtonSlider: return "SLIDER"; break; + default: return "ERROR"; break; + } + } + + const char *type_str_() { + switch (type) { + case TypeTouch: return "TOUCH"; break; + case TypeRelease: return "RELEASE"; break; + default: return "ERROR"; + } + } +}; + class FrontPanelHAL : public Component, public i2c::I2CDevice { public: - void set_trigger_pin(GPIOPin *pin) { i2c_trigger_pin_ = pin; } + FrontPanelEvent ev; + + void set_trigger_pin(GPIOPin *pin) { 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( + trigger_pin_->setup(); + trigger_pin_->attach_interrupt( FrontPanelHAL::isr, this, FALLING); } void dump_config() { ESP_LOGCONFIG(TAG, "I2C"); - LOG_PIN(" Interrupt pin: ", this->i2c_trigger_pin_); + LOG_PIN(" Interrupt pin: ", trigger_pin_); } void loop() { - if (this->queue_length > 0) { - this->queue_length--; - ESP_LOGD(TAG, "EVENT!"); + if (queue_length_ > 0) { + queue_length_ = 0; + read_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_; + GPIOPin *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; + volatile int queue_length_ = 0; + + uint8_t message[MSG_LEN]; + + void read_event_() { + if (!write_bytes_raw(READY_FOR_EV, MSG_LEN)) { + return; + } + if (!read_bytes_raw(message, MSG_LEN)) { + return; + } + + ev.parse(message); + ev.log(); + } }; /** @@ -54,7 +194,7 @@ protected: * on this counter. */ void ICACHE_RAM_ATTR HOT FrontPanelHAL::isr(FrontPanelHAL *store) { - store->queue_length++; + store->queue_length_++; } } // namespace bs2 diff --git a/light/automation.h b/light/automation.h index fc5fb17..594e448 100644 --- a/light/automation.h +++ b/light/automation.h @@ -13,10 +13,13 @@ class BrightnessTrigger : public Trigger { public: explicit BrightnessTrigger(YeelightBS2LightOutput *parent) { parent->add_on_state_callback([this](light::LightColorValues values) { - auto new_brightness = values.get_state() == 0 ? 0.0f : values.get_brightness(); + auto new_brightness = values.get_brightness(); + if (values.get_state() == 0) { + new_brightness = 0.0f; + } new_brightness = roundf(new_brightness * 100.0f) / 100.0f; if (last_brightness_ != new_brightness) { - this->trigger(new_brightness); + trigger(new_brightness); last_brightness_ = new_brightness; } }); diff --git a/light/light_output.h b/light/light_output.h index 4385a91..bc4fef4 100644 --- a/light/light_output.h +++ b/light/light_output.h @@ -40,7 +40,7 @@ public: } void add_on_state_callback(std::function &&callback) { - this->state_callback_.add(std::move(callback)); + state_callback_.add(std::move(callback)); } /** @@ -81,7 +81,7 @@ public: if (values.get_state() == 0) light_->turn_off(); - this->state_callback_.call(values); + state_callback_.call(values); } protected: @@ -116,10 +116,10 @@ public: output->set_transformer_inspector(this); } - bool is_active() { return this->transformer_ != nullptr; } - bool is_transition() { return this->transformer_->is_transition(); } - light::LightColorValues get_end_values() { return this->transformer_->get_end_values(); } - float get_progress() { return this->transformer_->get_progress(); } + 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 bs2