mirror of
https://github.com/esphome/esphome.git
synced 2026-09-03 19:46:02 +00:00
Add AC Dimmer support (#880)
* Add AC Dimmer support Fixes https://github.com/esphome/feature-requests/issues/278 * fixes basically missed the output pin setup and in the switching was switching true true true :P * Format * Enable ESP32 * Also setup ZC pin * Support multiple dimmers sharing ZC pin * Fix ESP32 * Lint * off gate on zc detect * tests pins validation * Climate Mitsubishi (#725) * add climate * Mitsubishi updates * refactor mitsubishi to use climate_ir * lint * fix: only decode when not str already (#923) Signed-off-by: wilmardo <info@wilmardenouden.nl> * fix climate-ir bad merge (#935) * fix climate-ir bad merge * add mitshubishi test * http_request: fix memory allocation (#916) * http_request version fix (#917) * PID Climate (#885) * PID Climate * Add sensor for debugging PID output value * Add dump_config, use percent * Add more observable values * Update * Set target temperature * Add autotuner * Add algorithm explanation * Add autotuner action, update controller * Add simulator * Format * Change defaults * Updates * Use b''.decode() instead of str(b'') (#941) Handling of request arguments in WizardRequestHandler is not decoding bytes and rather just doing a str conversion resulting in a value of "b''" being supplied to the wizard code. * Adding the espressif 2.6.3 (#944) * extract and use current version of python 3 (#938) * Inverted output in neopixelbus (#895) * Added inverted output * Added support for inverted output in neopixelbus * Update esphome/components/neopixelbus/light.py Co-Authored-By: Otto Winter <otto@otto-winter.com> * Update light.py * corrected lint errors Co-authored-by: Otto Winter <otto@otto-winter.com> * Added degree symbol for MAX7219 7-segment display. (#764) The ascii char to use it is "~" (0x7E). Disclaimer: I didn't test this yet. * Fix dump/tx of 64 bit codes (#940) * Fix dump/tx of 64 bit codes * fixed source format * Update hdc1080.cpp (#887) * Update hdc1080.cpp increase waittime, to fix reading errors * Fix: Update HDC1080.cpp i fixed the my change on write_bytes * add tcl112 support for dry, fan and swing (#939) * Fix SGP30 incorrect baseline reading/writing (#936) * Split the SGP30 baseline into 2 values - According to the SGP30 datasheet, each eCO2 and TVOC baseline is a 2-byte value (MSB first) - The current implementation ignores the MSB of each of the value - Update the schema to allow 2 different baseline values (optional, but both need to be specified for the baseline to apply) * Make both eCO2 and TVOC required if the optional baseline is defined * Make dump_config() looks better * Add register_*_effect to allow registering custom effects (#947) This allows to register custom effect from user components, allowing for bigger composability of source. * Bugfix/normalize core comparisons (and Python 3 update fixes) (#952) * Correct implementation of comparisons to be Pythonic If a comparison cannot be made return NotImplemented, this allows the Python interpreter to try other comparisons (eg __ieq__) and either return False (in the case of __eq__) or raise a TypeError exception (eg in the case of __lt__). * Python 3 updates * Add a more helpful message in exception if platform is not defined * Added a basic pre-commit check * Add transmit pioneer (#922) * Added pioneer_protocol to support transmit_pioneer * Display tm1637 (#946) * add TM1637 support * Support a further variant of Xiaomi CGG1 (#930) * Daikin climate ir component (#964) * Daikin ARC43XXX IR remote controller support * Format and lint fixes * Check temperature values against allowed min/max * fix tm1637 missing __init__.py (#975) * Add AC Dimmer support Fixes https://github.com/esphome/feature-requests/issues/278 * fixes basically missed the output pin setup and in the switching was switching true true true :P * Format * Enable ESP32 * Also setup ZC pin * Support multiple dimmers sharing ZC pin * Fix ESP32 * Lint * off gate on zc detect * tests pins validation * fix esp8266 many dimmers, changed timing * Increased value resolution, added min power * use min_power from base class * fix min_power. add init with half cycle * added method for trailing pulse, trailing and leading * fix method name. try filter invalid falling pulse * renamed to ac_dimmer * fix ESP32 not configuring zero cross twice Co-authored-by: Guillermo Ruffino <glm.net@gmail.com> Co-authored-by: Wilmar den Ouden <wilmardo@users.noreply.github.com> Co-authored-by: Nikolay Vasilchuk <Anonym.tsk@gmail.com> Co-authored-by: Tim Savage <tim@savage.company> Co-authored-by: Vc <37367415+Valcob@users.noreply.github.com> Co-authored-by: gitolicious <mrjchn@gmail.com> Co-authored-by: voibit <krestean@gmail.com> Co-authored-by: Luar Roji <cyberplant@users.noreply.github.com> Co-authored-by: András Bíró <1202136+andrasbiro@users.noreply.github.com> Co-authored-by: dmkif <dmkif@users.noreply.github.com> Co-authored-by: Panuruj Khambanonda (PK) <pk@panurujk.com> Co-authored-by: Kamil Trzciński <ayufan@ayufan.eu> Co-authored-by: Keith Burzinski <kbx81x@gmail.com> Co-authored-by: Mario <4376789+mario-tux@users.noreply.github.com> Co-authored-by: Héctor Giménez <hector.fwbz@gmail.com>
This commit is contained in:
co-authored by
Guillermo Ruffino
Wilmar den Ouden
Nikolay Vasilchuk
Tim Savage
Vc
gitolicious
voibit
Luar Roji
András Bíró
dmkif
Panuruj Khambanonda
Kamil Trzciński
Keith Burzinski
Mario
Héctor Giménez
parent
17fd9d5107
commit
835079ad43
@@ -0,0 +1,217 @@
|
||||
#include "ac_dimmer.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
#ifdef ARDUINO_ARCH_ESP8266
|
||||
#include <core_esp8266_waveform.h>
|
||||
#endif
|
||||
|
||||
namespace esphome {
|
||||
namespace ac_dimmer {
|
||||
|
||||
static const char *TAG = "ac_dimmer";
|
||||
|
||||
// Global array to store dimmer objects
|
||||
static AcDimmerDataStore *all_dimmers[32];
|
||||
|
||||
/// Time in microseconds the gate should be held high
|
||||
/// 10µs should be long enough for most triacs
|
||||
/// For reference: BT136 datasheet says 2µs nominal (page 7)
|
||||
static uint32_t GATE_ENABLE_TIME = 10;
|
||||
|
||||
/// Function called from timer interrupt
|
||||
/// Input is current time in microseconds (micros())
|
||||
/// Returns when next "event" is expected in µs, or 0 if no such event known.
|
||||
uint32_t ICACHE_RAM_ATTR HOT AcDimmerDataStore::timer_intr(uint32_t now) {
|
||||
// If no ZC signal received yet.
|
||||
if (this->crossed_zero_at == 0)
|
||||
return 0;
|
||||
|
||||
uint32_t time_since_zc = now - this->crossed_zero_at;
|
||||
if (this->value == 65535 || this->value == 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (this->enable_time_us != 0 && time_since_zc >= this->enable_time_us) {
|
||||
this->enable_time_us = 0;
|
||||
this->gate_pin->digital_write(true);
|
||||
// Prevent too short pulses
|
||||
this->disable_time_us = max(this->disable_time_us, time_since_zc + GATE_ENABLE_TIME);
|
||||
}
|
||||
if (this->disable_time_us != 0 && time_since_zc >= this->disable_time_us) {
|
||||
this->disable_time_us = 0;
|
||||
this->gate_pin->digital_write(false);
|
||||
}
|
||||
|
||||
if (time_since_zc < this->enable_time_us)
|
||||
// Next event is enable, return time until that event
|
||||
return this->enable_time_us - time_since_zc;
|
||||
else if (time_since_zc < disable_time_us) {
|
||||
// Next event is disable, return time until that event
|
||||
return this->disable_time_us - time_since_zc;
|
||||
}
|
||||
|
||||
if (time_since_zc >= this->cycle_time_us) {
|
||||
// Already past last cycle time, schedule next call shortly
|
||||
return 100;
|
||||
}
|
||||
|
||||
return this->cycle_time_us - time_since_zc;
|
||||
}
|
||||
|
||||
/// Run timer interrupt code and return in how many µs the next event is expected
|
||||
uint32_t ICACHE_RAM_ATTR HOT timer_interrupt() {
|
||||
// run at least with 1kHz
|
||||
uint32_t min_dt_us = 1000;
|
||||
uint32_t now = micros();
|
||||
for (auto *dimmer : all_dimmers) {
|
||||
if (dimmer == nullptr)
|
||||
// no more dimmers
|
||||
break;
|
||||
uint32_t res = dimmer->timer_intr(now);
|
||||
if (res != 0 && res < min_dt_us)
|
||||
min_dt_us = res;
|
||||
}
|
||||
// return time until next timer1 interrupt in µs
|
||||
return min_dt_us;
|
||||
}
|
||||
|
||||
/// GPIO interrupt routine, called when ZC pin triggers
|
||||
void ICACHE_RAM_ATTR HOT AcDimmerDataStore::gpio_intr() {
|
||||
uint32_t prev_crossed = this->crossed_zero_at;
|
||||
|
||||
// 50Hz mains frequency should give a half cycle of 10ms a 60Hz will give 8.33ms
|
||||
// in any case the cycle last at least 5ms
|
||||
this->crossed_zero_at = micros();
|
||||
uint32_t cycle_time = this->crossed_zero_at - prev_crossed;
|
||||
if (cycle_time > 5000) {
|
||||
this->cycle_time_us = cycle_time;
|
||||
} else {
|
||||
// Otherwise this is noise and this is 2nd (or 3rd...) fall in the same pulse
|
||||
// Consider this is the right fall edge and accumulate the cycle time instead
|
||||
this->cycle_time_us += cycle_time;
|
||||
}
|
||||
|
||||
if (this->value == 65535) {
|
||||
// fully on, enable output immediately
|
||||
this->gate_pin->digital_write(true);
|
||||
} else if (this->init_cycle) {
|
||||
// send a full cycle
|
||||
this->init_cycle = false;
|
||||
this->enable_time_us = 0;
|
||||
this->disable_time_us = cycle_time_us;
|
||||
} else if (this->value == 0) {
|
||||
// fully off, disable output immediately
|
||||
this->gate_pin->digital_write(false);
|
||||
} else {
|
||||
if (this->method == DIM_METHOD_TRAILING) {
|
||||
this->enable_time_us = 1; // cannot be 0
|
||||
this->disable_time_us = max((uint32_t) 10, this->value * this->cycle_time_us / 65535);
|
||||
} else {
|
||||
// calculate time until enable in µs: (1.0-value)*cycle_time, but with integer arithmetic
|
||||
// also take into account min_power
|
||||
auto min_us = this->cycle_time_us * this->min_power / 1000;
|
||||
this->enable_time_us = max((uint32_t) 1, ((65535 - this->value) * (this->cycle_time_us - min_us)) / 65535);
|
||||
if (this->method == DIM_METHOD_LEADING_PULSE) {
|
||||
// Minimum pulse time should be enough for the triac to trigger when it is close to the ZC zone
|
||||
// this is for brightness near 99%
|
||||
this->disable_time_us = max(this->enable_time_us + GATE_ENABLE_TIME, (uint32_t) cycle_time_us / 10);
|
||||
} else {
|
||||
this->gate_pin->digital_write(false);
|
||||
this->disable_time_us = this->cycle_time_us;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ICACHE_RAM_ATTR HOT AcDimmerDataStore::s_gpio_intr(AcDimmerDataStore *store) {
|
||||
// Attaching pin interrupts on the same pin will override the previous interupt
|
||||
// However, the user expects that multiple dimmers sharing the same ZC pin will work.
|
||||
// We solve this in a bit of a hacky way: On each pin interrupt, we check all dimmers
|
||||
// if any of them are using the same ZC pin, and also trigger the interrupt for *them*.
|
||||
for (auto *dimmer : all_dimmers) {
|
||||
if (dimmer == nullptr)
|
||||
break;
|
||||
if (dimmer->zero_cross_pin_number == store->zero_cross_pin_number) {
|
||||
dimmer->gpio_intr();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#ifdef ARDUINO_ARCH_ESP32
|
||||
// ESP32 implementation, uses basically the same code but needs to wrap
|
||||
// timer_interrupt() function to auto-reschedule
|
||||
static hw_timer_t *dimmer_timer = nullptr;
|
||||
void ICACHE_RAM_ATTR HOT AcDimmerDataStore::s_timer_intr() { timer_interrupt(); }
|
||||
#endif
|
||||
|
||||
void AcDimmer::setup() {
|
||||
// extend all_dimmers array with our dimmer
|
||||
|
||||
// Need to be sure the zero cross pin is setup only once, ESP8266 fails and ESP32 seems to fail silently
|
||||
auto setup_zero_cross_pin = true;
|
||||
|
||||
for (auto &all_dimmer : all_dimmers) {
|
||||
if (all_dimmer == nullptr) {
|
||||
all_dimmer = &this->store_;
|
||||
break;
|
||||
}
|
||||
if (all_dimmer->zero_cross_pin_number == this->zero_cross_pin_->get_pin()) {
|
||||
setup_zero_cross_pin = false;
|
||||
}
|
||||
}
|
||||
|
||||
this->gate_pin_->setup();
|
||||
this->store_.gate_pin = this->gate_pin_->to_isr();
|
||||
this->store_.zero_cross_pin_number = this->zero_cross_pin_->get_pin();
|
||||
this->store_.min_power = static_cast<uint16_t>(this->min_power_ * 1000);
|
||||
this->min_power_ = 0;
|
||||
this->store_.method = this->method_;
|
||||
|
||||
if (setup_zero_cross_pin) {
|
||||
this->zero_cross_pin_->setup();
|
||||
this->store_.zero_cross_pin = this->zero_cross_pin_->to_isr();
|
||||
this->zero_cross_pin_->attach_interrupt(&AcDimmerDataStore::s_gpio_intr, &this->store_, FALLING);
|
||||
}
|
||||
|
||||
#ifdef ARDUINO_ARCH_ESP8266
|
||||
// Uses ESP8266 waveform (soft PWM) class
|
||||
// PWM and AcDimmer can even run at the same time this way
|
||||
setTimer1Callback(&timer_interrupt);
|
||||
#endif
|
||||
#ifdef ARDUINO_ARCH_ESP32
|
||||
// 80 Divider -> 1 count=1µs
|
||||
dimmer_timer = timerBegin(0, 80, true);
|
||||
timerAttachInterrupt(dimmer_timer, &AcDimmerDataStore::s_timer_intr, true);
|
||||
// For ESP32, we can't use dynamic interval calculation because the timerX functions
|
||||
// are not callable from ISR (placed in flash storage).
|
||||
// Here we just use an interrupt firing every 50 µs.
|
||||
timerAlarmWrite(dimmer_timer, 50, true);
|
||||
timerAlarmEnable(dimmer_timer);
|
||||
#endif
|
||||
}
|
||||
void AcDimmer::write_state(float state) {
|
||||
auto new_value = static_cast<uint16_t>(roundf(state * 65535));
|
||||
if (new_value != 0 && this->store_.value == 0)
|
||||
this->store_.init_cycle = this->init_with_half_cycle_;
|
||||
this->store_.value = new_value;
|
||||
}
|
||||
void AcDimmer::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "AcDimmer:");
|
||||
LOG_PIN(" Output Pin: ", this->gate_pin_);
|
||||
LOG_PIN(" Zero-Cross Pin: ", this->zero_cross_pin_);
|
||||
ESP_LOGCONFIG(TAG, " Min Power: %.1f%%", this->store_.min_power / 10.0f);
|
||||
ESP_LOGCONFIG(TAG, " Init with half cycle: %s", YESNO(this->init_with_half_cycle_));
|
||||
if (method_ == DIM_METHOD_LEADING_PULSE)
|
||||
ESP_LOGCONFIG(TAG, " Method: leading pulse");
|
||||
else if (method_ == DIM_METHOD_LEADING)
|
||||
ESP_LOGCONFIG(TAG, " Method: leading");
|
||||
else
|
||||
ESP_LOGCONFIG(TAG, " Method: trailing");
|
||||
|
||||
LOG_FLOAT_OUTPUT(this);
|
||||
ESP_LOGV(TAG, " Estimated Frequency: %.3fHz", 1e6f / this->store_.cycle_time_us / 2);
|
||||
}
|
||||
|
||||
} // namespace ac_dimmer
|
||||
} // namespace esphome
|
||||
@@ -0,0 +1,66 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/esphal.h"
|
||||
#include "esphome/components/output/float_output.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace ac_dimmer {
|
||||
|
||||
enum DimMethod { DIM_METHOD_LEADING_PULSE = 0, DIM_METHOD_LEADING, DIM_METHOD_TRAILING };
|
||||
|
||||
struct AcDimmerDataStore {
|
||||
/// Zero-cross pin
|
||||
ISRInternalGPIOPin *zero_cross_pin;
|
||||
/// Zero-cross pin number - used to share ZC pin across multiple dimmers
|
||||
uint8_t zero_cross_pin_number;
|
||||
/// Output pin to write to
|
||||
ISRInternalGPIOPin *gate_pin;
|
||||
/// Value of the dimmer - 0 to 65535.
|
||||
uint16_t value;
|
||||
/// Minimum power for activation
|
||||
uint16_t min_power;
|
||||
/// Time between the last two ZC pulses
|
||||
uint32_t cycle_time_us;
|
||||
/// Time (in micros()) of last ZC signal
|
||||
uint32_t crossed_zero_at;
|
||||
/// Time since last ZC pulse to enable gate pin. 0 means not set.
|
||||
uint32_t enable_time_us;
|
||||
/// Time since last ZC pulse to disable gate pin. 0 means no disable.
|
||||
uint32_t disable_time_us;
|
||||
/// Set to send the first half ac cycle complete
|
||||
bool init_cycle;
|
||||
/// Dimmer method
|
||||
DimMethod method;
|
||||
|
||||
uint32_t timer_intr(uint32_t now);
|
||||
|
||||
void gpio_intr();
|
||||
static void s_gpio_intr(AcDimmerDataStore *store);
|
||||
#ifdef ARDUINO_ARCH_ESP32
|
||||
static void s_timer_intr();
|
||||
#endif
|
||||
};
|
||||
|
||||
class AcDimmer : public output::FloatOutput, public Component {
|
||||
public:
|
||||
void setup() override;
|
||||
|
||||
void dump_config() override;
|
||||
void set_gate_pin(GPIOPin *gate_pin) { gate_pin_ = gate_pin; }
|
||||
void set_zero_cross_pin(GPIOPin *zero_cross_pin) { zero_cross_pin_ = zero_cross_pin; }
|
||||
void set_init_with_half_cycle(bool init_with_half_cycle) { init_with_half_cycle_ = init_with_half_cycle; }
|
||||
void set_method(DimMethod method) { method_ = method; }
|
||||
|
||||
protected:
|
||||
void write_state(float state) override;
|
||||
|
||||
GPIOPin *gate_pin_;
|
||||
GPIOPin *zero_cross_pin_;
|
||||
AcDimmerDataStore store_;
|
||||
bool init_with_half_cycle_;
|
||||
DimMethod method_;
|
||||
};
|
||||
|
||||
} // namespace ac_dimmer
|
||||
} // namespace esphome
|
||||
@@ -0,0 +1,43 @@
|
||||
import esphome.codegen as cg
|
||||
import esphome.config_validation as cv
|
||||
from esphome import pins
|
||||
from esphome.components import output
|
||||
from esphome.const import CONF_ID, CONF_MIN_POWER, CONF_METHOD
|
||||
|
||||
ac_dimmer_ns = cg.esphome_ns.namespace('ac_dimmer')
|
||||
AcDimmer = ac_dimmer_ns.class_('AcDimmer', output.FloatOutput, cg.Component)
|
||||
|
||||
DimMethod = ac_dimmer_ns.enum('DimMethod')
|
||||
DIM_METHODS = {
|
||||
'LEADING_PULSE': DimMethod.DIM_METHOD_LEADING_PULSE,
|
||||
'LEADING': DimMethod.DIM_METHOD_LEADING,
|
||||
'TRAILING': DimMethod.DIM_METHOD_TRAILING,
|
||||
}
|
||||
|
||||
CONF_GATE_PIN = 'gate_pin'
|
||||
CONF_ZERO_CROSS_PIN = 'zero_cross_pin'
|
||||
CONF_INIT_WITH_HALF_CYCLE = 'init_with_half_cycle'
|
||||
CONFIG_SCHEMA = output.FLOAT_OUTPUT_SCHEMA.extend({
|
||||
cv.Required(CONF_ID): cv.declare_id(AcDimmer),
|
||||
cv.Required(CONF_GATE_PIN): pins.internal_gpio_output_pin_schema,
|
||||
cv.Required(CONF_ZERO_CROSS_PIN): pins.internal_gpio_input_pin_schema,
|
||||
cv.Optional(CONF_INIT_WITH_HALF_CYCLE, default=True): cv.boolean,
|
||||
cv.Optional(CONF_METHOD, default='leading pulse'): cv.enum(DIM_METHODS, upper=True, space='_'),
|
||||
}).extend(cv.COMPONENT_SCHEMA)
|
||||
|
||||
|
||||
def to_code(config):
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
yield cg.register_component(var, config)
|
||||
|
||||
# override default min power to 10%
|
||||
if CONF_MIN_POWER not in config:
|
||||
config[CONF_MIN_POWER] = 0.1
|
||||
yield output.register_output(var, config)
|
||||
|
||||
pin = yield cg.gpio_pin_expression(config[CONF_GATE_PIN])
|
||||
cg.add(var.set_gate_pin(pin))
|
||||
pin = yield cg.gpio_pin_expression(config[CONF_ZERO_CROSS_PIN])
|
||||
cg.add(var.set_zero_cross_pin(pin))
|
||||
cg.add(var.set_init_with_half_cycle(config[CONF_INIT_WITH_HALF_CYCLE]))
|
||||
cg.add(var.set_method(config[CONF_METHOD]))
|
||||
@@ -1012,6 +1012,10 @@ output:
|
||||
id: id24
|
||||
pin: GPIO26
|
||||
period: 15s
|
||||
- platform: ac_dimmer
|
||||
id: dimmer1
|
||||
gate_pin: GPIO5
|
||||
zero_cross_pin: GPIO26
|
||||
|
||||
light:
|
||||
- platform: binary
|
||||
|
||||
@@ -649,6 +649,10 @@ output:
|
||||
return {s};
|
||||
outputs:
|
||||
- id: custom_float
|
||||
- platform: ac_dimmer
|
||||
id: dimmer1
|
||||
gate_pin: GPIO5
|
||||
zero_cross_pin: GPIO12
|
||||
|
||||
mcp23017:
|
||||
id: mcp23017_hub
|
||||
|
||||
Reference in New Issue
Block a user