Browse Source

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.

pull/4/head
Maurice Makaay 3 years ago
parent
commit
e5612e8235
6 changed files with 200 additions and 219 deletions
  1. +71
    -88
      __init__.py
  2. +62
    -0
      front_panel_hal.h
  3. +13
    -12
      light/__init__.py
  4. +6
    -6
      light/light_output.h
  5. +48
    -0
      light_hal.h
  6. +0
    -113
      yeelight_bs2_hub.h

+ 71
- 88
__init__.py View File

@ -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)

+ 62
- 0
front_panel_hal.h View File

@ -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

+ 13
- 12
light/__init__.py View File

@ -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)


+ 6
- 6
light/light_output.h View File

@ -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<void(light::LightColorValues)> state_callback_{};


+ 48
- 0
light_hal.h View File

@ -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

+ 0
- 113
yeelight_bs2_hub.h View File

@ -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

Loading…
Cancel
Save