diff --git a/esphome/components/ct_clamp/__init__.py b/esphome/components/ct_clamp/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/ct_clamp/ct_clamp_sensor.cpp b/esphome/components/ct_clamp/ct_clamp_sensor.cpp new file mode 100644 index 0000000000..1a7bac2844 --- /dev/null +++ b/esphome/components/ct_clamp/ct_clamp_sensor.cpp @@ -0,0 +1,67 @@ +#include "ct_clamp_sensor.h" + +#include "esphome/core/log.h" +#include + +namespace esphome { +namespace ct_clamp { + +static const char *TAG = "ct_clamp"; + +void CTClampSensor::dump_config() { + LOG_SENSOR("", "CT Clamp Sensor", this); + ESP_LOGCONFIG(TAG, " Sample Duration: %.2fs", this->sample_duration_ / 1e3f); + LOG_UPDATE_INTERVAL(this); +} + +void CTClampSensor::update() { + // Update only starts the sampling phase, in loop() the actual sampling is happening. + + // Request a high loop() execution interval during sampling phase. + this->high_freq_.start(); + + // Set timeout for ending sampling phase + this->set_timeout("read", this->sample_duration_, [this]() { + this->is_sampling_ = false; + this->high_freq_.stop(); + + if (this->num_samples_ == 0) { + // Shouldn't happen, but let's not crash if it does. + this->publish_state(NAN); + return; + } + + float raw = this->sample_sum_ / this->num_samples_; + float irms = std::sqrt(raw); + ESP_LOGD(TAG, "'%s' - Raw Value: %.2fA", this->name_.c_str(), irms); + this->publish_state(irms); + }); + + // Set sampling values + this->is_sampling_ = true; + this->num_samples_ = 0; + this->sample_sum_ = 0.0f; +} + +void CTClampSensor::loop() { + if (!this->is_sampling_) + return; + + // Perform a single sample + float value = this->source_->sample(); + + // Adjust DC offset via low pass filter (exponential moving average) + const float alpha = 0.001f; + this->offset_ = this->offset_ * (1 - alpha) + value * alpha; + + // Filtered value centered around the mid-point (0V) + float filtered = value - this->offset_; + + // IRMS is sqrt(∑v_i²) + float sq = filtered * filtered; + this->sample_sum_ += sq; + this->num_samples_++; +} + +} // namespace ct_clamp +} // namespace esphome diff --git a/esphome/components/ct_clamp/ct_clamp_sensor.h b/esphome/components/ct_clamp/ct_clamp_sensor.h new file mode 100644 index 0000000000..d816ac781a --- /dev/null +++ b/esphome/components/ct_clamp/ct_clamp_sensor.h @@ -0,0 +1,46 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/esphal.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/voltage_sampler/voltage_sampler.h" + +namespace esphome { +namespace ct_clamp { + +class CTClampSensor : public sensor::Sensor, public PollingComponent { + public: + void update() override; + void loop() override; + void dump_config() override; + float get_setup_priority() const override { return setup_priority::DATA; } + + void set_sample_duration(uint32_t sample_duration) { sample_duration_ = sample_duration; } + void set_source(voltage_sampler::VoltageSampler *source) { source_ = source; } + + protected: + /// High Frequency loop() requester used during sampling phase. + HighFrequencyLoopRequester high_freq_; + + /// Duration in ms of the sampling phase. + uint32_t sample_duration_; + /// The sampling source to read values from. + voltage_sampler::VoltageSampler *source_; + + /** The DC offset of the circuit. + * + * Diagram: https://learn.openenergymonitor.org/electricity-monitoring/ct-sensors/interface-with-arduino + * + * This is automatically calculated with an exponential moving average/digital low pass filter. + * + * 0.5 is a good initial approximation to start with for most ESP8266 setups. + */ + float offset_ = 0.5f; + + float sample_sum_ = 0.0f; + uint32_t num_samples_ = 0; + bool is_sampling_ = false; +}; + +} // namespace ct_clamp +} // namespace esphome diff --git a/esphome/components/ct_clamp/sensor.py b/esphome/components/ct_clamp/sensor.py new file mode 100644 index 0000000000..9f41f8c614 --- /dev/null +++ b/esphome/components/ct_clamp/sensor.py @@ -0,0 +1,27 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import sensor, voltage_sampler +from esphome.const import CONF_SENSOR, CONF_ID, ICON_FLASH, UNIT_AMPERE + +AUTO_LOAD = ['voltage_sampler'] + +CONF_SAMPLE_DURATION = 'sample_duration' + +ct_clamp_ns = cg.esphome_ns.namespace('ct_clamp') +CTClampSensor = ct_clamp_ns.class_('CTClampSensor', sensor.Sensor, cg.PollingComponent) + +CONFIG_SCHEMA = sensor.sensor_schema(UNIT_AMPERE, ICON_FLASH, 2).extend({ + cv.GenerateID(): cv.declare_id(CTClampSensor), + cv.Required(CONF_SENSOR): cv.use_id(voltage_sampler.VoltageSampler), + cv.Optional(CONF_SAMPLE_DURATION, default='200ms'): cv.positive_time_period_milliseconds, +}).extend(cv.polling_component_schema('60s')) + + +def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + yield cg.register_component(var, config) + yield sensor.register_sensor(var, config) + + sens = yield cg.get_variable(config[CONF_SENSOR]) + cg.add(var.set_source(sens)) + cg.add(var.set_sample_duration(config[CONF_SAMPLE_DURATION])) diff --git a/tests/test3.yaml b/tests/test3.yaml index fb46043f34..6b64af8981 100644 --- a/tests/test3.yaml +++ b/tests/test3.yaml @@ -129,6 +129,19 @@ sensor: b_constant: 3950 reference_resistance: 10k reference_temperature: 25°C + - platform: ntc + sensor: resist + name: NTC Sensor2 + calibration: + - 10.0kOhm -> 25°C + - 27.219kOhm -> 0°C + - 14.674kOhm -> 15°C + - platform: ct_clamp + sensor: my_sensor + name: CT Clamp + sample_duration: 500ms + update_interval: 5s + - platform: tcs34725 red_channel: name: Red Channel