dimmer: Add support for OXT 1/2-channel light dimmer

This commit is contained in:
michau 2023-12-23 14:52:35 +01:00 committed by michau
parent cefbfb75bd
commit a2cff7c486
6 changed files with 381 additions and 0 deletions

View file

@ -305,6 +305,7 @@ esphome/components/online_image/* @guillempages
esphome/components/opentherm/* @olegtarasov
esphome/components/ota/* @esphome/core
esphome/components/output/* @esphome/core
esphome/components/oxt_dimmer/* @michau-krakow
esphome/components/pca6416a/* @Mat931
esphome/components/pca9554/* @clydebarrow @hwstar
esphome/components/pcf85063/* @brogon

View file

@ -0,0 +1,68 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome import pins
from esphome.components import light, uart
from esphome.const import (
CONF_CHANNELS,
CONF_GAMMA_CORRECT,
CONF_OUTPUT_ID,
CONF_MIN_VALUE,
CONF_MAX_VALUE,
CONF_SENSING_PIN,
)
CODEOWNERS = ["@michau-krakow"]
DEPENDENCIES = ["uart", "light"]
oxt_dimmer_ns = cg.esphome_ns.namespace("oxt_dimmer")
OxtController = oxt_dimmer_ns.class_("OxtController", cg.Component, uart.UARTDevice)
OxtDimmerChannel = oxt_dimmer_ns.class_(
"OxtDimmerChannel", cg.Component, light.LightOutput
)
CHANNEL_SCHEMA = light.BRIGHTNESS_ONLY_LIGHT_SCHEMA.extend(
{
cv.GenerateID(CONF_OUTPUT_ID): cv.declare_id(OxtDimmerChannel),
cv.Optional(CONF_MIN_VALUE, default=50): cv.int_range(min=0, max=255),
cv.Optional(CONF_MAX_VALUE, default=255): cv.int_range(min=0, max=255),
cv.Optional(CONF_SENSING_PIN): pins.gpio_input_pin_schema,
# override defaults
cv.Optional(CONF_GAMMA_CORRECT, default=1.0): cv.positive_float,
}
)
CONFIG_SCHEMA = (
# allow at least 15ms for UART frame of 10-12 bytes @9600bps to be fully transmitted!
cv.polling_component_schema("50 ms")
.extend(
{
cv.GenerateID(CONF_OUTPUT_ID): cv.declare_id(OxtController),
cv.Required(CONF_CHANNELS): cv.All(
cv.ensure_list(CHANNEL_SCHEMA), cv.Length(min=1, max=2)
),
}
)
.extend(uart.UART_DEVICE_SCHEMA)
)
FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema(
"oxt_dimmer", baud_rate=9600, require_tx=True, require_rx=False
)
async def to_code(config):
ctrl = cg.new_Pvariable(config[CONF_OUTPUT_ID])
await cg.register_component(ctrl, config)
await uart.register_uart_device(ctrl, config)
for index, channel_cfg in enumerate(config[CONF_CHANNELS]):
channel = cg.new_Pvariable(channel_cfg[CONF_OUTPUT_ID])
await cg.register_component(channel, channel_cfg)
await light.register_light(channel, channel_cfg)
cg.add(channel.set_min_value(channel_cfg[CONF_MIN_VALUE]))
cg.add(channel.set_max_value(channel_cfg[CONF_MAX_VALUE]))
if CONF_SENSING_PIN in channel_cfg:
sensing_pin = await cg.gpio_pin_expression(channel_cfg[CONF_SENSING_PIN])
cg.add(channel.set_sensing_pin(sensing_pin))
cg.add(ctrl.add_channel(index, channel))

View file

@ -0,0 +1,173 @@
/*
Copyright © 2023
*/
/*********************************************************************************************\
* The OXT dimmer uses simple protocol over serial @9600bps where each frame looks like:
* 0 1 2 3 4 5 6 7 8 9 A B
* 00 00 ff 55 - Header
* 01 - Channel being updated
* 4b - Light brightness of channel 1 [00..ff]
* 4c - Light brightness of channel 2 [00..ff]
* 05 dc 0a 00 00 - Footer
\*********************************************************************************************/
#include "oxt_dimmer.h"
namespace esphome {
namespace oxt_dimmer {
static const char *const TAG = "oxt";
void OxtController::update() {
for (const auto &channel : channels_) {
if (channel)
channel->update_sensing_input();
}
}
void OxtController::send_to_mcu_(const OxtDimmerChannel *updated_channel) {
struct {
uint8_t header[3];
uint8_t update;
uint8_t channel[2];
uint8_t footer[4];
} frame{{0x00, 0xff, 0x55}, 0x00, {0x00, 0x00}, {0x05, 0xdc, 0x0a, 0x00}};
for (size_t index = 0; index < MAX_CHANNELS; index++) {
auto *channel = channels_[index];
if (channel == nullptr)
continue;
auto binary = channel->is_on();
auto brightness = channel->brightness();
if (binary) {
frame.channel[index] = brightness;
}
if (channel == updated_channel) {
frame.update = index + 1;
ESP_LOGI(TAG, "Setting channel %u state=%s, raw brightness=%d", index, ONOFF(binary), brightness);
}
}
if (frame.update == 0) {
ESP_LOGE(TAG, "Unable to find channel index");
return;
}
ESP_LOGV(TAG, "Frame: %s", format_hex_pretty(reinterpret_cast<uint8_t *>(&frame), sizeof(frame)).c_str());
this->write_array(reinterpret_cast<uint8_t *>(&frame), sizeof(frame));
}
light::LightTraits OxtDimmerChannel::get_traits() {
auto traits = light::LightTraits();
traits.set_supported_color_modes({light::ColorMode::BRIGHTNESS});
return traits;
}
void OxtDimmerChannel::write_state(light::LightState *state) {
if (controller_ == nullptr) {
ESP_LOGE(TAG, "No controller - state change ignored");
return;
}
bool binary;
float brightness;
// Fill our variables with the device's current state
state->current_values_as_binary(&binary);
state->current_values_as_brightness(&brightness);
// Convert ESPHome's brightness (0-1) to the internal brightness (0-255)
const uint8_t calculated_brightness = remap<uint8_t, float>(brightness, 0.0, 1.0f, min_value_, max_value_);
if (calculated_brightness == 0) {
binary = false;
}
// If a new value, write to the dimmer
if (binary != binary_ || calculated_brightness != brightness_) {
brightness_ = calculated_brightness;
binary_ = binary;
controller_->send_to_mcu_(this);
}
}
void OxtDimmerChannel::short_press_() {
ESP_LOGI(TAG, "short_press");
light_state_->toggle().perform();
}
void OxtDimmerChannel::periodic_long_press_() {
// Note: This function is operating on ESPHome brightness values in range 0-1 float
float brightness;
light_state_->current_values_as_brightness(&brightness);
ESP_LOGD(TAG, "brightness: %0.2f, direction: %d, millis %u", brightness, sensing_state_.direction_, millis());
brightness = clamp(brightness + sensing_state_.direction_ * 0.02f, 0.0f, 1.0f);
ESP_LOGI(TAG, "next brightness: %0.2f", brightness);
light_state_->make_call()
.set_brightness(brightness)
.set_state((brightness > 0))
.set_transition_length({}) // cancel transition, if any
.perform();
}
void OxtDimmerChannel::update_sensing_input() {
if (!sensing_state_.sensing_pin_)
return;
bool btn_pressed = sensing_state_.sensing_pin_->digital_read();
switch (sensing_state_.state_) {
case SensingStateT::STATE_RELEASED:
if (btn_pressed) {
sensing_state_.millis_pressed_ = millis();
sensing_state_.state_ = SensingStateT::STATE_DEBOUNCING;
}
break;
case SensingStateT::STATE_DEBOUNCING:
if (!btn_pressed) {
sensing_state_.millis_pressed_ = 0;
sensing_state_.state_ = SensingStateT::STATE_RELEASED;
} else if (millis() - sensing_state_.millis_pressed_ > 50) {
sensing_state_.state_ = SensingStateT::STATE_PRESSED;
}
break;
case SensingStateT::STATE_PRESSED:
if (!btn_pressed) {
short_press_();
sensing_state_.state_ = SensingStateT::STATE_RELEASED;
} else if (millis() - sensing_state_.millis_pressed_ > 1000) {
sensing_state_.state_ = SensingStateT::STATE_LONGPRESS;
sensing_state_.direction_ *= -1;
}
break;
case SensingStateT::STATE_LONGPRESS:
if (btn_pressed) {
periodic_long_press_();
} else
sensing_state_.state_ = SensingStateT::STATE_RELEASED;
break;
default:
ESP_LOGE(TAG, "should never get here");
}
}
void OxtDimmerChannel::dump_config() {
ESP_LOGCONFIG(TAG, "OXT channel: '%s'", light_state_ ? light_state_->get_name().c_str() : "");
ESP_LOGCONFIG(TAG, " Minimal brightness: %d", min_value_);
ESP_LOGCONFIG(TAG, " Maximal brightness: %d", max_value_);
ESP_LOGCONFIG(TAG, " Sensing pin: %s",
sensing_state_.sensing_pin_ ? sensing_state_.sensing_pin_->dump_summary().c_str() : "none");
}
void OxtController::dump_config() { ESP_LOGCONFIG(TAG, "Oxt dimmer"); }
} // namespace oxt_dimmer
} // namespace esphome

View file

@ -0,0 +1,97 @@
#pragma once
/*
Copyright © 2023
*/
#include "esphome/core/log.h"
#include "esphome/core/helpers.h"
#include "esphome/core/component.h"
#include "esphome/components/uart/uart.h"
#include "esphome/components/light/light_output.h"
#include "esphome/components/light/light_state.h"
#include "esphome/components/light/light_traits.h"
namespace esphome {
namespace oxt_dimmer {
class OxtController;
/**
* OxtDimmerChannel inherits from light::LightOutput and provides "light"
* functionality towards front-end, ESPHome, HASS...
*/
class OxtDimmerChannel : public light::LightOutput, public Component {
public:
// Component overrides
void dump_config() override;
// LightOutput overrides
light::LightTraits get_traits() override;
void setup_state(light::LightState *state) override { light_state_ = state; }
void write_state(light::LightState *state) override;
// Own methods
bool is_on() { return binary_; }
uint8_t brightness() { return brightness_; }
void set_min_value(const uint8_t min_value) { min_value_ = min_value; }
void set_max_value(const uint8_t max_value) { max_value_ = max_value; }
void set_sensing_pin(GPIOPin *sensing_pin) { sensing_state_.sensing_pin_ = sensing_pin; }
void set_controller(OxtController *control) { controller_ = control; }
void update_sensing_input();
protected:
OxtController *controller_{nullptr};
struct SensingStateT {
enum { STATE_RELEASED, STATE_DEBOUNCING, STATE_PRESSED, STATE_LONGPRESS } state_ = STATE_RELEASED;
uint32_t millis_pressed_ = 0;
int direction_ = 1;
GPIOPin *sensing_pin_;
} sensing_state_;
// light implementation
uint8_t min_value_{50};
uint8_t max_value_{255};
bool binary_{false};
uint8_t brightness_{0};
light::LightState *light_state_{nullptr};
void short_press_();
void periodic_long_press_();
};
/**
* OxtController class takes care of communication with dimming MCU (back-end)
* and polling external switch(es) using GPIO input pins
*/
class OxtController : public uart::UARTDevice, public PollingComponent {
friend class OxtDimmerChannel;
public:
static constexpr size_t MAX_CHANNELS = 2;
// Component methods
void dump_config() override;
float get_setup_priority() const override { return esphome::setup_priority::DATA; }
// PollingComponent methods
void update() override;
// Own methods
void add_channel(uint8_t index, OxtDimmerChannel *channel) {
channels_[index] = channel;
channel->set_controller(this);
}
protected:
void send_to_mcu_(const OxtDimmerChannel *channel);
private:
OxtDimmerChannel *channels_[MAX_CHANNELS]{nullptr};
};
} // namespace oxt_dimmer
} // namespace esphome

42
tests/test12.yaml Normal file
View file

@ -0,0 +1,42 @@
# Tests for OXT dimmer using bk7xx board
---
esphome:
name: bk72xx-with-oxt-dimmer-test
bk72xx:
board: cb3s
wifi:
ssid: "ssid"
ota:
captive_portal:
# Disable UART logging - we need UART to talk to dimming MCU
logger:
baud_rate: 0
level: VERBOSE
uart:
tx_pin: GPIO11
baud_rate: 9600
light:
- platform: oxt_dimmer
channels:
- name: lamp1
id: lamp1
min_value: 0
max_value: 255
sensing_pin:
number: GPIO08
inverted: true
mode: input_pullup
- name: lamp2
id: lamp2
sensing_pin:
number: GPIO09
inverted: true
mode: input_pullup
default_transition_length: 1s