Browse Source

Basic compatibility established. Configuration needs more work though.

pull/56/head
Maurice Makaay 3 years ago
parent
commit
52825c345c
4 changed files with 93 additions and 119 deletions
  1. +27
    -78
      components/xiaomi_bslamp2/__init__.py
  2. +60
    -37
      components/xiaomi_bslamp2/front_panel_hal.h
  3. +4
    -3
      components/xiaomi_bslamp2/light/color_handler_rgb.h
  4. +2
    -1
      components/xiaomi_bslamp2/sensor/slider_sensor.h

+ 27
- 78
components/xiaomi_bslamp2/__init__.py View File

@ -3,32 +3,26 @@ import esphome.config_validation as cv
from esphome import pins from esphome import pins
from esphome.components.ledc.output import LEDCOutput from esphome.components.ledc.output import LEDCOutput
from esphome.components.gpio.output import GPIOBinaryOutput 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 ( 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"] 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 = "master1"
CONF_MASTER1_ID = "master1_id"
CONF_MASTER2 = "master2" 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_LIGHT_HAL_ID = "light_hal_id"
CONF_FRONT_PANEL_HAL_ID = "front_panel_hal_id" CONF_FRONT_PANEL_HAL_ID = "front_panel_hal_id"
CONF_ON_BRIGHTNESS = "on_brightness" CONF_ON_BRIGHTNESS = "on_brightness"
CONF_LEDS = "leds" CONF_LEDS = "leds"
AUTO_LOAD = ["ledc", "output", "i2c"]
xiaomi_ns = cg.esphome_ns.namespace("xiaomi") xiaomi_ns = cg.esphome_ns.namespace("xiaomi")
bslamp2_ns = xiaomi_ns.namespace("bslamp2") bslamp2_ns = xiaomi_ns.namespace("bslamp2")
LightHAL = bslamp2_ns.class_("LightHAL", cg.Component) LightHAL = bslamp2_ns.class_("LightHAL", cg.Component)
@ -55,85 +49,40 @@ FRONT_PANEL_LED_OPTIONS = {
CONFIG_SCHEMA = cv.COMPONENT_SCHEMA.extend({ CONFIG_SCHEMA = cv.COMPONENT_SCHEMA.extend({
# RGBWW Light # RGBWW Light
cv.GenerateID(CONF_LIGHT_HAL_ID): cv.declare_id(LightHAL), 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 # Front panel I2C
cv.GenerateID(CONF_FRONT_PANEL_HAL_ID): cv.declare_id(FrontPanelHAL), 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): 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_HAL_ID])
await cg.register_component(light_hal, config) 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): 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_HAL_ID])
await cg.register_component(fp_hal, config) 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)) 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): 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_light_hal(config)
await make_front_panel_hal(config) await make_front_panel_hal(config)

+ 60
- 37
components/xiaomi_bslamp2/front_panel_hal.h View File

@ -3,7 +3,7 @@
#include "common.h" #include "common.h"
#include "esphome/components/i2c/i2c.h" #include "esphome/components/i2c/i2c.h"
#include "esphome/core/component.h" #include "esphome/core/component.h"
#include "esphome/core/esphal.h"
#include "esphome/core/log.h"
#include <array> #include <array>
#include <cmath> #include <cmath>
@ -100,7 +100,7 @@ class FrontPanelEventParser {
// All events use the prefix [04:04:01:00]. // All events use the prefix [04:04:01:00].
if (m[0] != 0x04 || m[1] != 0x04 || m[2] != 0x01 || m[3] != 0x00) { 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. // 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])) else if (m[5] == 0x02 && m[6] == (0x03 + m[4]))
ev |= FLAG_TYPE_RELEASE; ev |= FLAG_TYPE_RELEASE;
else else
return error_(ev, m, "invalid event type for button");
return this->error_(ev, m, "invalid event type for button");
break; break;
case 0x03: // slider touch case 0x03: // slider touch
case 0x04: // slider release case 0x04: // slider release
ev |= FLAG_PART_SLIDER; ev |= FLAG_PART_SLIDER;
ev |= (m[4] == 0x03 ? FLAG_TYPE_TOUCH : FLAG_TYPE_RELEASE); ev |= (m[4] == 0x03 ? FLAG_TYPE_TOUCH : FLAG_TYPE_RELEASE);
if ((m[6] - m[5] - m[4] - 0x01) != 0) 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) 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 { else {
auto level = 0x17 - m[5]; auto level = 0x17 - m[5];
ev |= (level << FLAG_LEVEL_SHIFT); ev |= (level << FLAG_LEVEL_SHIFT);
} }
break; break;
default: default:
return error_(ev, m, "invalid part id");
return this->error_(ev, m, "invalid part id");
return ev; return ev;
} }
@ -148,8 +148,8 @@ class FrontPanelEventParser {
ESP_LOGE(TAG, "Front panel I2C event error:"); ESP_LOGE(TAG, "Front panel I2C event error:");
ESP_LOGE(TAG, " Error: %s", msg); 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, " 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)) { if (has_(ev, FLAG_PART_MASK, FLAG_PART_SLIDER)) {
auto level = (ev & FLAG_LEVEL_MASK) >> FLAG_LEVEL_SHIFT; auto level = (ev & FLAG_LEVEL_MASK) >> FLAG_LEVEL_SHIFT;
if (level > 0) { if (level > 0) {
@ -160,7 +160,7 @@ class FrontPanelEventParser {
return ev; return ev;
} }
const char *format_part(EVENT ev) {
const char *format_part_(EVENT ev) {
if (has_(ev, FLAG_PART_MASK, FLAG_PART_POWER)) if (has_(ev, FLAG_PART_MASK, FLAG_PART_POWER))
return "power button"; return "power button";
if (has_(ev, FLAG_PART_MASK, FLAG_PART_COLOR)) if (has_(ev, FLAG_PART_MASK, FLAG_PART_COLOR))
@ -170,7 +170,7 @@ class FrontPanelEventParser {
return "n/a"; 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)) if (has_(ev, FLAG_TYPE_MASK, FLAG_TYPE_TOUCH))
return "touch"; return "touch";
if (has_(ev, FLAG_TYPE_MASK, FLAG_TYPE_RELEASE)) 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 * This is a hardware abstraction layer that communicates with with front
* panel of the Xiaomi Mijia Bedside Lamp 2. * 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 * 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. * 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<void(EVENT)> &&callback) { event_callback_.add(std::move(callback)); }
void add_on_event_callback(std::function<void(EVENT)> &&callback) {
event_callback_.add(std::move(callback));
}
void setup() { void setup() {
ESP_LOGCONFIG(TAG, "Setting up I2C trigger pin interrupt..."); 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() { void dump_config() {
ESP_LOGCONFIG(TAG, "FrontPanelHAL:"); ESP_LOGCONFIG(TAG, "FrontPanelHAL:");
LOG_I2C_DEVICE(this);
LOG_PIN(" I2C interrupt pin: ", trigger_pin_); LOG_PIN(" I2C interrupt pin: ", trigger_pin_);
} }
void loop() { void loop() {
// Read and publish front panel events. // 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; 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_) { if (led_state_ != last_led_state_) {
update_leds();
update_leds();
} }
} }
@ -268,7 +302,7 @@ class FrontPanelHAL : public Component, public i2c::I2CDevice {
void update_leds() { void update_leds() {
led_msg_[2] = led_state_ >> 8; led_msg_[2] = led_state_ >> 8;
led_msg_[3] = led_state_ & 0xff; led_msg_[3] = led_state_ & 0xff;
write_bytes_raw(led_msg_, MSG_LEN);
write(led_msg_, MSG_LEN);
last_led_state_ = led_state_; last_led_state_ = led_state_;
} }
@ -298,9 +332,8 @@ class FrontPanelHAL : public Component, public i2c::I2CDevice {
} }
protected: protected:
GPIOPin *trigger_pin_;
static void isr(FrontPanelHAL *store);
volatile int event_id_ = 0;
InternalGPIOPin *trigger_pin_;
FrontPanelTriggerStore store_{};
int last_event_id_ = 0; int last_event_id_ = 0;
CallbackManager<void(EVENT)> event_callback_{}; CallbackManager<void(EVENT)> event_callback_{};
@ -309,16 +342,6 @@ class FrontPanelHAL : public Component, public i2c::I2CDevice {
MSG led_msg_ = {0x02, 0x03, 0x00, 0x00, 0x64, 0x00, 0x00}; 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 bslamp2
} // namespace xiaomi } // namespace xiaomi
} // namespace esphome } // namespace esphome

+ 4
- 3
components/xiaomi_bslamp2/light/color_handler_rgb.h View File

@ -2,6 +2,7 @@
#include <array> #include <array>
#include <cmath> #include <cmath>
#include <algorithm>
#include "../common.h" #include "../common.h"
#include "../light_hal.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 // 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 // and 7, determining in what ring of the RGB circle the requested
// color resides. // 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; auto level = 7.0f * rgb_min;
// While the default color circle in Home Assistant presents only a // 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). * Returns the position on an RGB ring in degrees (0 - 359).
*/ */
float ring_pos_(float red, float green, float blue) { 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; auto delta = rgb_max - rgb_min;
float pos; float pos;
if (delta == 0.0f) if (delta == 0.0f)


+ 2
- 1
components/xiaomi_bslamp2/sensor/slider_sensor.h View File

@ -4,6 +4,7 @@
#include "../front_panel_hal.h" #include "../front_panel_hal.h"
#include "esphome/components/sensor/sensor.h" #include "esphome/components/sensor/sensor.h"
#include <cmath> #include <cmath>
#include <algorithm>
namespace esphome { namespace esphome {
namespace xiaomi { 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 // look like this one was ever meant to be used, or that
// the design was faulty on this. Therefore, level 1 is // the design was faulty on this. Therefore, level 1 is
// ignored. The resulting range of levels is 0-19. // 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); float final_level = range_from_ + (slope_ * corrected_level);


Loading…
Cancel
Save