diff --git a/esphome/components/ac_dimmer/__init__.py b/esphome/components/ac_dimmer/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/esphome/components/ac_dimmer/ac_dimmer.cpp b/esphome/components/ac_dimmer/ac_dimmer.cpp
new file mode 100644
index 0000000000..a60cc9e29a
--- /dev/null
+++ b/esphome/components/ac_dimmer/ac_dimmer.cpp
@@ -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
diff --git a/esphome/components/ac_dimmer/ac_dimmer.h b/esphome/components/ac_dimmer/ac_dimmer.h
new file mode 100644
index 0000000000..00da061cfd
--- /dev/null
+++ b/esphome/components/ac_dimmer/ac_dimmer.h
@@ -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
diff --git a/esphome/components/ac_dimmer/output.py b/esphome/components/ac_dimmer/output.py
new file mode 100644
index 0000000000..16f04ac984
--- /dev/null
+++ b/esphome/components/ac_dimmer/output.py
@@ -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]))
diff --git a/tests/test1.yaml b/tests/test1.yaml
index 8483281b59..16fc382b2f 100644
--- a/tests/test1.yaml
+++ b/tests/test1.yaml
@@ -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
diff --git a/tests/test3.yaml b/tests/test3.yaml
index e7c3da18de..2a37095aca 100644
--- a/tests/test3.yaml
+++ b/tests/test3.yaml
@@ -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