mirror of
https://github.com/esphome/esphome.git
synced 2024-12-02 19:54:14 +01:00
835079ad43
* 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>
217 lines
7.8 KiB
C++
217 lines
7.8 KiB
C++
#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
|