mirror of
https://github.com/esphome/esphome.git
synced 2024-11-27 09:18:00 +01:00
dimmer: Add support for OXT 1/2-channel light dimmer
This commit is contained in:
parent
cefbfb75bd
commit
a2cff7c486
6 changed files with 381 additions and 0 deletions
|
@ -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
|
||||
|
|
0
esphome/components/oxt_dimmer/__init__.py
Normal file
0
esphome/components/oxt_dimmer/__init__.py
Normal file
68
esphome/components/oxt_dimmer/light.py
Normal file
68
esphome/components/oxt_dimmer/light.py
Normal 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))
|
173
esphome/components/oxt_dimmer/oxt_dimmer.cpp
Normal file
173
esphome/components/oxt_dimmer/oxt_dimmer.cpp
Normal 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
|
97
esphome/components/oxt_dimmer/oxt_dimmer.h
Normal file
97
esphome/components/oxt_dimmer/oxt_dimmer.h
Normal 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
42
tests/test12.yaml
Normal 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
|
Loading…
Reference in a new issue