Added AC Voltage Sensor. Reading results are still wrong

This commit is contained in:
Chris Stuurman 2026-01-19 21:52:11 +02:00
parent 56124beb98
commit 6611e6c32f
5 changed files with 460 additions and 321 deletions

View File

View File

@ -0,0 +1,73 @@
#include "ac_voltage_sensor.h"
#include "esphome/core/log.h"
#include <cinttypes>
#include <cmath>
namespace esphome {
namespace ac_voltage_sensor {
static const char *const TAG = "ac_voltage_sensor";
void AC_Voltage_Sensor::dump_config() {
LOG_SENSOR("", "AC Voltage Sensor", this);
ESP_LOGCONFIG(TAG, " Sample Duration: %.2fs", this->sample_duration_ / 1e3f);
LOG_UPDATE_INTERVAL(this);
}
void AC_Voltage_Sensor::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;
}
const float rms_ac_dc_squared = this->sample_squared_sum_ / this->num_samples_;
const float rms_dc = this->sample_sum_ / this->num_samples_;
const float rms_ac_squared = rms_ac_dc_squared - rms_dc * rms_dc;
float rms_ac = 0;
if (rms_ac_squared > 0)
rms_ac = std::sqrt(rms_ac_squared);
ESP_LOGD(TAG, "'%s' - Raw AC Value: %.3fV, DC: %.3fV after %" PRIu32 " different samples (%" PRIu32 " SPS)",
this->name_.c_str(), rms_ac, rms_dc, this->num_samples_, 1000 * this->num_samples_ / this->sample_duration_);
this->publish_state(rms_ac);
});
// Set sampling values
this->last_value_ = 0.0;
this->num_samples_ = 0;
this->sample_sum_ = 0.0f;
this->sample_squared_sum_ = 0.0f;
this->is_sampling_ = true;
}
void AC_Voltage_Sensor::loop() {
if (!this->is_sampling_)
return;
// Perform a single sample
float value = this->source_->sample();
if (std::isnan(value))
return;
// Assuming a sine wave, avoid requesting values faster than the ADC can provide them
if (this->last_value_ == value)
return;
this->last_value_ = value;
this->num_samples_++;
this->sample_sum_ += value;
this->sample_squared_sum_ += value * value;
}
} // namespace ac_voltage_sensor
} // namespace esphome

View File

@ -0,0 +1,52 @@
#pragma once
#include "esphome/core/component.h"
#include "esphome/core/hal.h"
#include "esphome/components/sensor/sensor.h"
#include "esphome/components/voltage_sampler/voltage_sampler.h"
namespace esphome {
namespace ac_voltage_sensor {
class AC_Voltage_Sensor : public sensor::Sensor, public PollingComponent {
public:
void update() override;
void loop() override;
void dump_config() override;
float get_setup_priority() const override {
// After the base sensor has been initialized
return setup_priority::DATA - 1.0f;
}
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 is removed from the AC value, i.e. the value in dc_component_ is subtracted from the sampled value
*
* Diagram: https://learn.openenergymonitor.org/electricity-monitoring/ct-sensors/interface-with-arduino
*
* The AC component is essentially the same as the calculating the Standard-Deviation,
* which can be done by cumulating 3 values per sample:
* 1) Number of samples
* 2) Sum of samples
* 3) Sum of sample squared
* https://en.wikipedia.org/wiki/Root_mean_square
*/
float last_value_ = 0.0f;
float sample_sum_ = 0.0f;
float sample_squared_sum_ = 0.0f;
uint32_t num_samples_ = 0;
bool is_sampling_ = false;
};
} // namespace ac_voltage_sensor
} // namespace esphome

View File

@ -0,0 +1,45 @@
import esphome.codegen as cg
from esphome.components import sensor, voltage_sampler
import esphome.config_validation as cv
from esphome.const import (
CONF_SENSOR,
DEVICE_CLASS_VOLTAGE,
STATE_CLASS_MEASUREMENT,
UNIT_VOLT,
)
# based on ctclamp by jesserockz
AUTO_LOAD = ["voltage_sampler"]
CODEOWNERS = ["@stuurmcp"]
CONF_SAMPLE_DURATION = "sample_duration"
ac_voltage_sensor_ns = cg.esphome_ns.namespace("ac_voltage_sensor")
AC_Voltage_Sensor = ac_voltage_sensor_ns.class_("AC_Voltage_Sensor", sensor.Sensor, cg.PollingComponent)
CONFIG_SCHEMA = (
sensor.sensor_schema(
AC_Voltage_Sensor,
unit_of_measurement=UNIT_VOLT,
accuracy_decimals=2,
device_class=DEVICE_CLASS_VOLTAGE,
state_class=STATE_CLASS_MEASUREMENT,
)
.extend(
{
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"))
)
async def to_code(config):
var = await sensor.new_sensor(config)
await cg.register_component(var, config)
sens = await cg.get_variable(config[CONF_SENSOR])
cg.add(var.set_source(sens))
cg.add(var.set_sample_duration(config[CONF_SAMPLE_DURATION]))

View File

@ -4,7 +4,7 @@ external_components:
- source:
type: local
path: components # Path relative to this YAML file
components: [ ads1115_int, tlc59208f_ext, ds3231 ] #, ads131m08 ]
components: [ ads1115_int, tlc59208f_ext, ds3231, ac_voltage_sensor ] #, ads131m08 ]
packages:
- !include common/wifi.yaml
@ -228,8 +228,10 @@ logger:
ds3231: DEBUG
tlc59208f_ext: VERBOSE
text_sensor: INFO
ads1115_int: VERBOSE
ads1115_int.sensor: VERBOSE
ads1115: DEBUG
ads1115_int: DEBUG
ads1115_int.sensor: DEBUG
ac_voltage_sensor: INFO
modbus: VERBOSE
modbus_controller: INFO
modbus_controller.sensor: INFO
@ -279,35 +281,36 @@ ads1115_int:
- address: 0x4A
id: ads1115_4A
i2c_id: bus_a
continuous_mode: true
interleaved_mode: true
continuous_mode: false
alert_rdy_pin:
number: GPIO27
mode:
input: true
pullup: false
ads1115:
- address: 0x49
id: ads1115_49
i2c_id: bus_a
interleaved_mode: true
continuous_mode: false
alert_rdy_pin:
number: GPIO5
mode:
input: true
pullup: false
continuous_mode: true
# interleaved_mode: true
# alert_rdy_pin:
# number: GPIO5
# mode:
# input: true
# pullup: false
- address: 0x48
id: ads1115_48
i2c_id: bus_a
interleaved_mode: true
continuous_mode: false
alert_rdy_pin:
number: GPIO3
mode:
input: true
pullup: false # external 10k pullup on ads1115 dev board
continuous_mode: true
# interleaved_mode: true
# alert_rdy_pin:
# number: GPIO3
# mode:
# input: true
# pullup: false # external 10k pullup on ads1115 dev board
spi:
- id: spi_bus0
@ -1543,353 +1546,319 @@ sensor:
name: "CPU Frequency"
# NB! Keep all ads1115 sample rates the same. Update intervals should be more than or equal to 1/sample_rate
- platform: ads1115_int
#resolution: 12_BITS
multiplexer: 'A0_A1'
gain: 2.048 # 4.096
ads1115_id: ads1115_48
sample_rate: 860 # 475 #860
state_class: measurement
device_class: current
accuracy_decimals: 8
name: "ADC House Current"
id: power_outlets_current
unit_of_measurement: "A"
icon: "mdi:current"
# update_interval: 8ms #5ms
#filters:
## - offset: 0.0002
# - lambda: return x * x;
# - sliding_window_moving_average:
# window_size: 1250 #1250 #5000
# send_every: 208 #208 #416
# send_first_at: 208 #208 #416
# - lambda: return sqrt(x);
# - multiply: 555 #95 #88.44
# - offset: 0.0 #-0.2
# - lambda: |-
# if(abs(x) < 0.1)
# return 0.0;
# return x;
# ads1115_48
- platform: ads1115_int
multiplexer: 'A2_A3'
gain: 2.048 # 4.096
ads1115_id: ads1115_48
name: "ADC Geyser Current"
id: geyser_current
sample_rate: 860 #860
state_class: measurement
device_class: current
accuracy_decimals: 8
unit_of_measurement: "A"
icon: "mdi:current"
filters:
- lambda: return x * x;
- sliding_window_moving_average:
window_size: 625 #1250 #5000
send_every: 104 #208 #416
send_first_at: 104 #208 #416
- lambda: return sqrt(x);
- multiply: 100 #92.1 #91.1 #88.44
- offset: 0.0 #-0.2
# - lambda: |-
# if(abs(x) < 0.1)
# return 0.0;
# return x;
on_value_range:
- below: 5.0
then:
- lambda: |-
ESP_LOGI("geyser", "No geyser current detected. Geyser not heating.");
- above: 5.0
then:
- lambda: |-
ESP_LOGI("geyser", "Geyser current detected. Geyser was energised.");
# ads1115_49
- platform: ads1115_int
multiplexer: A0_A1
gain: 2.048 # 4.096
ads1115_id: ads1115_49
name: "ADC Lights Current"
id: lights_current
sample_rate: 860
# update_interval: 10ms
# id: lights_current_adc
state_class: measurement
device_class: current
accuracy_decimals: 8
unit_of_measurement: "A"
icon: "mdi:current"
filters:
- lambda: return x * x;
- sliding_window_moving_average:
window_size: 625 #1250 #5000
send_every: 104 #208 #416
send_first_at: 104 #208 #416
- lambda: return sqrt(x);
- multiply: 100 #92.1 #91.1 #88.44
- offset: 0.0 #-0.2
# - lambda: |-
# if(abs(x) < 0.1)
# return 0.0;
# return x;
- platform: ads1115_int
multiplexer: 'A2_A3'
gain: 2.048 # 4.096
ads1115_id: ads1115_49
name: "ADC Mains Current"
id: mains_current
sample_rate: 860 #475
state_class: measurement
device_class: current
accuracy_decimals: 8
unit_of_measurement: "A"
icon: "mdi:current"
#update_interval: 8ms #5ms
#filters:
## - offset: 0.0002
# - lambda: return x * x;
# - sliding_window_moving_average:
# window_size: 1250 #1250 #5000
# send_every: 208 #208 #416
# send_first_at: 208 #208 #416
# - lambda: return sqrt(x);
# - multiply: 95 #88.44
# - offset: 0.0 #-0.2
# - lambda: |-
# if(abs(x) < 0.1)
# return 0.0;
# return x;
# - name: "ADC House Current"
# platform: ads1115_int
# multiplexer: 'A0_A1'
# gain: 2.048
# ads1115_id: ads1115_48
# sample_rate: 860
# state_class: measurement
# device_class: current
# accuracy_decimals: 8
# id: power_outlets_current
# unit_of_measurement: "A"
# icon: "mdi:current"
# filters:
# - lambda: return x * x;
# - sliding_window_moving_average:
# window_size: 1250
# send_every: 208
# send_first_at: 208
# - lambda: return sqrt(x);
# - multiply: 105
#
# - name: "ADC Geyser Current"
# platform: ads1115_int
# multiplexer: 'A2_A3'
# gain: 2.048
# ads1115_id: ads1115_48
# id: geyser_current
# sample_rate: 860
# state_class: measurement
# device_class: current
# accuracy_decimals: 8
# unit_of_measurement: "A"
# icon: "mdi:current"
# filters:
# - lambda: return x * x;
# - sliding_window_moving_average:
# window_size: 1250
# send_every: 208
# send_first_at: 208
# - lambda: return sqrt(x);
# - multiply: 105
# on_value_range:
# - below: 5.0
# then:
# - lambda: |-
# ESP_LOGI("geyser", "No geyser current detected. Geyser not heating.");
# - above: 5.0
# then:
# - lambda: |-
# ESP_LOGI("geyser", "Geyser current detected. Geyser was energised.");
#
# - name: "ADC Lights Current"
# platform: ads1115_int
# multiplexer: A0_A1
# gain: 2.048
# ads1115_id: ads1115_49
# id: lights_current
# sample_rate: 860
# # id: lights_current_adc
# state_class: measurement
# device_class: current
# accuracy_decimals: 8
# unit_of_measurement: "A"
# icon: "mdi:current"
# filters:
# - lambda: return x * x;
# - sliding_window_moving_average:
# window_size: 1250
# send_every: 208
# send_first_at: 208
# - lambda: return sqrt(x);
# - multiply: 98
#
# - name: "ADC Mains Current"
# platform: ads1115_int
# multiplexer: 'A2_A3'
# gain: 2.048
# ads1115_id: ads1115_49
# id: mains_current
# sample_rate: 860
# state_class: measurement
# device_class: current
# accuracy_decimals: 8
# unit_of_measurement: "A"
# icon: "mdi:current"
# filters:
# - lambda: return x * x;
# - sliding_window_moving_average:
# window_size: 1250
# send_every: 208
# send_first_at: 208
# - lambda: return sqrt(x);
# - multiply: 100
#
# ads1115_4A
# Inverter voltage sensor
- platform: ads1115_int
- name: "ADC Mains Voltage"
platform: ads1115_int
ads1115_id: ads1115_4A
sample_rate: 860
name: "ADC Mains Voltage"
id: mains_voltage_adc
unit_of_measurement: "V"
accuracy_decimals: 8
icon: "mdi:flash"
multiplexer: A0_GND
gain: 2.048 # 4.096
#update_interval: 8ms #5ms #23ms
gain: 2.048
device_class: voltage
state_class: measurement
# filters:
# - offset: -2.048 #-2.04794027 # 0.0131
# - lambda: return x * x;
# - sliding_window_moving_average:
# window_size: 1250 #1250
# send_every: 208
# send_first_at: 208
# - lambda: return sqrt(x);
# - multiply: 766.6670 # 930 #650
#- lambda: |-
# if(abs(x) < 10)
# return 0;
# return x;#
filters:
- offset: -2.048
- lambda: return x * x;
- sliding_window_moving_average:
window_size: 1250
send_every: 208
send_first_at: 208
- lambda: return sqrt(x);
- multiply: 653
- platform: ads1115_int
- name: "ADC Spare1 Voltage"
platform: ads1115_int
ads1115_id: ads1115_4A
sample_rate: 860
name: "ADC Spare1 Voltage"
unit_of_measurement: "V"
accuracy_decimals: 8
icon: "mdi:flash"
multiplexer: A1_GND
gain: 2.048 # 4.096
#update_interval: 8ms #5ms #23ms
gain: 2.048
device_class: voltage
state_class: measurement
- platform: ads1115_int
- name: "ADC House Voltage"
platform: ads1115_int
ads1115_id: ads1115_4A
sample_rate: 860
name: "ADC House Voltage"
id: inverter_output_voltage_adc
unit_of_measurement: "V"
accuracy_decimals: 8
icon: "mdi:flash"
multiplexer: A2_GND
gain: 2.048 # 4.096
gain: 2.048
device_class: voltage
state_class: measurement
# filters:
# - offset: -2.048 #-2.0491 #4.096 #0.0065
# - lambda: return x * x;
# - sliding_window_moving_average:
# window_size: 1250 #625 #1250
# send_every: 208 #104
# send_first_at: 208 #104 #416
# - lambda: return sqrt(x);
# - multiply: 766.6670 # 930 #650
#- lambda: |-
# if(abs(x) < 20)
# return 0;
# return x;
filters:
- offset: -2.048
- lambda: return x * x;
- sliding_window_moving_average:
window_size: 1250
send_every: 208
send_first_at: 208
- lambda: return sqrt(x);
- multiply: 653
- platform: ads1115_int
- name: "ADC Spare2 Voltage"
platform: ads1115_int
ads1115_id: ads1115_4A
sample_rate: 860
name: "ADC Spare2 Voltage"
unit_of_measurement: "V"
accuracy_decimals: 8
icon: "mdi:flash"
multiplexer: A3_GND
gain: 2.048 # 4.096
#update_interval: 8ms #5ms #23ms
gain: 2.048
device_class: voltage
state_class: measurement
#
######################################################## CT CLAMP ########################################################
- platform: ads1115
multiplexer: 'A0_A1'
gain: 2.048
ads1115_id: ads1115_48
sample_rate: 860
id: adc_house_current
- platform: ads1115
multiplexer: 'A2_A3'
gain: 2.048
ads1115_id: ads1115_48
id: adc_geyser_current
sample_rate: 860
- platform: ads1115
multiplexer: A0_A1
gain: 2.048
ads1115_id: ads1115_49
id: adc_lights_current
sample_rate: 860
- platform: ads1115
multiplexer: 'A2_A3'
gain: 2.048
ads1115_id: ads1115_49
id: adc_mains_current
sample_rate: 860
# 30A clamp
- platform: ct_clamp
sensor: adc_geyser_current
id: geyser_current
name: "ACL Geyser Current"
update_interval: 5s
sample_duration: 4s
state_class: measurement
device_class: current
filters:
# burden resistor is 62Ω in parallel with 33Ω = 21.54Ω
# multiplier should be 1860/21.54 = x86.35
- multiply: 88.51 #88.51 = real world
on_value_range:
- below: 0.5
then:
- lambda: |-
ESP_LOGI("geyser", "Geyser lost power.");
- above: 0.5
then:
- lambda: |-
ESP_LOGI("geyser", "Geyser was energised.");
# 30A clamp
- platform: ct_clamp
sensor: adc_lights_current
id: lights_current
name: "ACL Lights Current"
update_interval: 5s
sample_duration: 4s
state_class: measurement
device_class: current
filters:
# burden resistor is 62Ω in parallel with 33Ω = 21.54Ω
# multiplier should be 1860/21.54 = x86.35
- multiply: 88.44 #88.44 = real world
# 100A clamp
- platform: ct_clamp
sensor: adc_mains_current
id: mains_current
name: "ACL Mains Current"
update_interval: 5s
sample_duration: 4s
state_class: measurement
device_class: current
filters:
# burden resistor is 22Ω
# multiplier should be 2000/22 = x90.9
- multiply: 90.25 # 90.25 = real world
# 100A clamp
- platform: ct_clamp
sensor: adc_house_current
id: power_outlets_current
name: "ACL House Current"
update_interval: 5s
sample_duration: 4s
state_class: measurement
device_class: current
filters:
# burden resistor is 22Ω
# multiplier should be 2000/22 = x90.9
- multiply: 91.14 #91.14 = real world
####################################################### EOF CT CLAMP #######################################################
# - platform: ads1115
# ads1115_id: ads1115_4A
# sample_rate: 860
# id: adc_mains_voltage
# multiplexer: A0_GND
# gain: 2.048
# - platform: ads1115
# ads1115_id: ads1115_4A
# sample_rate: 860
# id: adc_spare1_voltage
# multiplexer: A1_GND
# gain: 2.048
# - platform: ads1115
# ads1115_id: ads1115_4A
# sample_rate: 860
# id: adc_house_voltage
# multiplexer: A2_GND
# gain: 2.048
# - platform: ads1115
# ads1115_id: ads1115_4A
# sample_rate: 860
# id: adc_spare2_voltage
# multiplexer: A3_GND
# gain: 2.048
#
## # 30A clamp
## - platform: ct_clamp
## sensor: geyser_current_adc
## id: geyser_current
## name: "Geyser Current"
## update_interval: 2s
## sample_duration: 2000ms #15000ms
## state_class: measurement
## device_class: current
## filters:
## # burden resistor is 62Ω in parallel with 33Ω = 21.54Ω
## # multiplier should be 1860/21.54 = x86.35
## - multiply: 88.51 # real world
## - lambda: |-
## if(x < 0.25)
## return 0.0;
## return x;
## on_value_range:
## - below: 0.5
## then:
## - lambda: |-
## ESP_LOGI("geyser", "Geyser lost power.");
## - above: 0.5
## then:
## - lambda: |-
## ESP_LOGI("geyser", "Geyser was energised.");
# - name: "ADC Mains Voltage"
# platform: ac_voltage_sensor
# sensor: adc_mains_voltage
# update_interval: 5s
# sample_duration: 4.5s
# filters:
# - multiply: 884
#
## # 30A clamp
## - platform: ct_clamp
## sensor: lights_current_adc
## id: lights_current
## name: "Lights Current"
## update_interval: 1s
## sample_duration: 1s #15000ms
## state_class: measurement
## device_class: current
## filters:
## # burden resistor is 62Ω in parallel with 33Ω = 21.54Ω
## # multiplier should be 1860/21.54 = x86.35
## - multiply: 88.44 # real world
## - lambda: |-
## if(x < 0.25)
## return 0.0;
## return x;
# - name: "ADC Spare1 Voltage"
# platform: ac_voltage_sensor
# sensor: adc_spare1_voltage
# update_interval: 5s
# sample_duration: 4.5s
# filters:
# - multiply: 884
#
## # 100A clamp
## - platform: ct_clamp
## sensor: mains_current_adc
## id: mains_current
## name: "Mains Current"
## update_interval: 1s
## sample_duration: 1s #15000ms
## state_class: measurement
## device_class: current
## filters:
## # burden resistor is 22Ω
## # multiplier should be 2000/22 = x90.9
## - multiply: 90.25 # real world
## - lambda: |-
## if(x < 0.25)
## return 0.0;
## return x;
##
## # 100A clamp
## - platform: ct_clamp
## sensor: power_outlets_current_adc
## id: power_outlets_current
## name: "Plugs Supply Current"
## update_interval: 1s
## sample_duration: 1s #15000ms
## state_class: measurement
## device_class: current
## filters:
## # burden resistor is 22Ω
## # multiplier should be 2000/22 = x90.9
## - multiply: 91.14 # real world
## - lambda: |-
## if(x < 0.25)
## return 0.0;
## return x;
# - name: "ADC House Voltage"
# platform: ac_voltage_sensor
# sensor: adc_house_voltage
# update_interval: 5s
# sample_duration: 4.5s
# filters:
# - multiply: 884
#
# - platform: template
# id: calibrate_lights
# name: "AAA Lights A"
# lambda: |-
# return id(lights_current).state;
# state_class: measurement
# device_class: current
# accuracy_decimals: 8
# update_interval: 1s
#
# - platform: template
# id: calibrate_mains
# name: "AAA Mains A"
# lambda: |-
# return id(mains_current).state;
# state_class: measurement
# device_class: current
# accuracy_decimals: 8
# update_interval: 1s
#
# - platform: template
# id: calibrate_mains_v
# name: "AAA Mains V"
# lambda: |-
# return id(mains_voltage_adc).state;
# state_class: measurement
# device_class: voltage
# accuracy_decimals: 8
# update_interval: 1s
#
# - platform: template
# id: calibrate_plugs
# name: "AAA Plugs A"
# lambda: |-
# return id(power_outlets_current).state;
# state_class: measurement
# device_class: current
# accuracy_decimals: 8
# update_interval: 1s
#
# - platform: template
# id: calibrate_plugs_V
# name: "AAA Plugs V"
# lambda: |-
# return id(inverter_output_voltage_adc).state;
# state_class: measurement
# device_class: voltage
# accuracy_decimals: 8
# update_interval: 1s
#
# - platform: template
# id: calibrate_geyser
# name: "AAA Geyser A"
# lambda: |-
# return id(geyser_current).state;
# state_class: measurement
# device_class: current
# accuracy_decimals: 8
# update_interval: 1s
# - name: "ADC Spare2 Voltage"
# platform: ac_voltage_sensor
# sensor: adc_spare2_voltage
# update_interval: 5s
# sample_duration: 4.5s
# filters:
# - multiply: 884
#
# for now we use a template until we get a voltage sensor
- platform: template