Phantom negative energy consumption from CT clamps connected via ESP32

I have CT clamps around both sides of my split-single-phase US residential service. I’ll probably post it in another place when I can identify one… I’m using the six channel board available through CircuitSetup but posting here because I suspect a lot of CircuitSetup boards wind up in Home Assistant installations.

I see phantom negative consumption and am trying to track down where it’s coming from. I kind of know where it’s coming from, I’m not exactly using the same grade of hardware my utility is using - the CircuitSetup gizmo gets my service input, car charger and two other circuits in my kitchen, and the rest of the inputs are provided from various smart plugs. I know I can’t get it to balance perfectly but want to get it… closer than it is now.

I have no solar or battery and it’s impossible to get those negative values shown, they’re just a rounding error of sorts I think.

I know that what I really need to do is run my Chevy volt down to empty then plug in the charger and confirm amperage being delivered according to the OBD-2 reader matches the two phases from the utility with everything else shut off. Haven’t found a convenient time to do that quite yet.

esphome:
  name: esphome-web-0d42f4
  friendly_name: Power Meter ESPHome 0d42f4
  min_version: 2024.11.0
  name_add_mac_suffix: false

# CircuitSetup 6 Channel Energy Meter Main Board example config
# See all options at https://esphome.io/components/sensor/atm90e32.html

substitutions:
# Change the disp_name to something you want  
  disp_name: Energy Meter
  friendly_name: My Energy Metering
# Interval of how often the power data is updated
  update_time: 10s
# Change current_cal to the corresponding CT's that you're using
# If different CTs per current channel, remove or change "${current_cal}" from 
# "gain_ct" below and replace with the CT calibration number respectively
# Current Transformers:
#  20A/25mA SCT-006: 11143
#  30A/1V SCT-013-030: 8650
#  50A/1V SCT-013-050: 15420
#  50A/16.6mA SCT-010: 41334
#  80A/26.6mA SCT-010: 41660
#  100A/50ma SCT-013-000: 27518
#  120A/40mA: SCT-016: 41787
#  200A/100mA SCT-024: 27518
#  200A/50mA SCT-024: 55036
  current_cal_ct1: '27518'
  current_cal_ct2: '27518'
  current_cal_ct3: '11143'
  current_cal_ct4: '8650'
  current_cal_ct5: '8650'
  current_cal_ct6: '11143'
# This only needs to be changed if you're using something other than the  
# Jameco 9VAC Transformer: 
  voltage_cal: '7305'

packages:
  common: github://CircuitSetup/Expandable-6-Channel-ESP32-Energy-Meter/Software/ESPHome/6chan_common.yaml



esp32:
  board: esp32dev
  framework:
    type: esp-idf

# Enable logging
logger:

# Enable Home Assistant API
api:

# Allow Over-The-Air updates
ota:
- platform: esphome

web_server:
  port: 80

spi:
  clk_pin: 18
  miso_pin: 19
  mosi_pin: 23

wifi:
 ssid: !secret wifi_ssid
 password: !secret wifi_password

bluetooth_proxy:
  active: true

sensor:
  - platform: wifi_signal
    name: DeepStateDaycare WiFi
    update_interval: 60s
  # IC1
  - platform: atm90e32
    cs_pin: 5
    phase_a:
      voltage:
        name: ${disp_name} Volts A
        id: ic1Volts
        accuracy_decimals: 1
      current:
        name: ${disp_name} CT1 Amps
        id: ct1Amps
      phase_angle:
        name: ${disp_name} CT1 Phase Angle
        id: ct1PA
      harmonic_power:
        name: ${disp_name} CT1 Harmonic Power
        id: ct1HP
      power:
        name: ${disp_name} CT1 Watts
        id: ct1Watts
      gain_voltage: ${voltage_cal}
      gain_ct: ${current_cal_ct1}
    phase_b:
      current:
        name: ${disp_name} CT2 Amps
        id: ct2Amps
      power:
        name: ${disp_name} CT2 Watts
        id: ct2Watts
      gain_voltage: ${voltage_cal}
      gain_ct: ${current_cal_ct2}
      phase_angle:
        name: ${disp_name} CT2 Phase Angle
        id: ct2PA
      harmonic_power:
        name: ${disp_name} CT2 Harmonic Power
        id: ct2HP
    phase_c:
      current:
        name: ${disp_name} CT3 Amps
        id: ct3Amps
      power:
        name: ${disp_name} CT3 Watts
        id: ct3Watts
      gain_voltage: ${voltage_cal}
      gain_ct: ${current_cal_ct3}
    frequency:
      name: ${disp_name} Freq A
    line_frequency: 60Hz
    gain_pga: 1X
    update_interval: ${update_time}
  # IC2
  - platform: atm90e32
    cs_pin: 4
    phase_a:
      voltage:
        name: ${disp_name} Volts B
        id: ic2Volts
        accuracy_decimals: 1
      current:
        name: ${disp_name} CT4 Amps
        id: ct4Amps
      power:
        name: ${disp_name} CT4 Watts
        id: ct4Watts
      gain_voltage: ${voltage_cal}
      gain_ct: ${current_cal_ct4}
    phase_b:
      current:
        name: ${disp_name} CT5 Amps
        id: ct5Amps
      power:
        name: ${disp_name} CT5 Watts
        id: ct5Watts
      gain_voltage: ${voltage_cal}
      gain_ct: ${current_cal_ct5}
    phase_c:
      current:
        name: ${disp_name} CT6 Amps
        id: ct6Amps
      power:
        name: ${disp_name} CT6 Watts
        id: ct6Watts
      gain_voltage: ${voltage_cal}
      gain_ct: ${current_cal_ct6}
    frequency:
      name: ${disp_name} Freq B
    line_frequency: 60Hz
    gain_pga: 1X
    update_interval: ${update_time}

  # Total Amps
  - platform: template
    name: ${disp_name} Total Amps
    id: totalAmps
    lambda: return id(ct1Amps).state + id(ct2Amps).state; 
    accuracy_decimals: 2
    unit_of_measurement: A
    device_class: current
    update_interval: ${update_time}

  # Total Watts
  - platform: template
    name: ${disp_name} Total Watts
    id: totalWatts
    lambda: return id(ct1Watts).state + id(ct2Watts).state ; 
    accuracy_decimals: 2
    unit_of_measurement: W
    device_class: power
    update_interval: ${update_time}

  # Total kWh
  - platform: template
    name: ${disp_name} Total Amps
    id: totalkWh
    lambda: return (id(ct1Amps).state + id(ct2Amps).state) * .001; 
    accuracy_decimals: 2
    unit_of_measurement: kWh
    device_class: current
    update_interval: ${update_time}    

  - platform: template
    name: ${disp_name} CT1 VA  # Apparent Power
    id: ct1VA
    lambda: return id(ic1Volts).state * id(ct1Amps).state;
    unit_of_measurement: VA
    accuracy_decimals: 2

  - platform: template
    name: ${disp_name} CT2 VA  # Apparent Power
    id: ct2VA
    lambda: return id(ic2Volts).state * id(ct2Amps).state;
    unit_of_measurement: VA
    accuracy_decimals: 2




  # kWh per channel
  - platform: total_daily_energy
    name: ${disp_name} CT1 kWh
    power_id: ct1Watts
    filters:
      - multiply: 0.001
    unit_of_measurement: kWh
    device_class: energy
    state_class: total_increasing

  - platform: total_daily_energy
    name: ${disp_name} CT2 kWh
    power_id: ct2Watts
    filters:
      - multiply: 0.001
    unit_of_measurement: kWh
    device_class: energy
    state_class: total_increasing

  - platform: total_daily_energy
    name: ${disp_name} CT3 kWh
    power_id: ct3Watts
    filters:
      - multiply: 0.001
    unit_of_measurement: kWh
    device_class: energy
    state_class: total_increasing

  - platform: total_daily_energy
    name: ${disp_name} CT4 kWh
    power_id: ct4Watts
    filters:
      - multiply: 0.001
    unit_of_measurement: kWh
    device_class: energy
    state_class: total_increasing

  - platform: total_daily_energy
    name: ${disp_name} CT5 kWh
    power_id: ct5Watts
    filters:
      - multiply: 0.001
    unit_of_measurement: kWh
    device_class: energy
    state_class: total_increasing

  - platform: total_daily_energy
    name: ${disp_name} CT6 kWh
    power_id: ct6Watts
    filters:
      - multiply: 0.001
    unit_of_measurement: kWh
    device_class: energy
    state_class: total_increasing

  - platform: template
    name: ${disp_name} CT1 Power Factor
    id: ct1PF
    lambda: |-
        {
            float watts = id(ct1Watts).state;
            float va = id(ct1VA).state;

            if (va == 0) {
                return 0.0;
            } else {
                float pf = watts / va;
                if (pf!= pf) {  // Direct NaN check
                    return 0.0;
                } else {
                    return pf;
                }
            }
        }
    unit_of_measurement: ""
    accuracy_decimals: 2
    update_interval: ${update_time}

  - platform: template
    name: ${disp_name} CT2 Power Factor
    id: ct2PF
    lambda: |-
        {
            float watts = id(ct2Watts).state;
            float va = id(ct2VA).state;

            if (va == 0) {
                return 0.0;
            } else {
                float pf = watts / va;
                if (pf!= pf) {  // Direct NaN check
                    return 0.0;
                } else {
                    return pf;
                }
            }
        }
    unit_of_measurement: ""
    accuracy_decimals: 2
    update_interval: ${update_time}

#kWh
#  - platform: total_daily_energy
#    name: ${disp_name} Total kWh
#    power_id: totalWatts
#    filters:
#      - multiply: 0.001
#    unit_of_measurement: kWh
#    device_class: energy
#    state_class: total_increasing

  - platform: template
    name: "Free Heap Memory"
    id: free_heap
    unit_of_measurement: "bytes"
    lambda: |-
      {
        #include <esp_heap_caps.h> 
        return esp_get_free_heap_size();
      }
    update_interval: 60s # Adjust as needed

switch:
  - platform: restart
    name: ${disp_name} Restart  
time:
  - platform: sntp
    id: sntp_time   

Here’s my power meter configuration from ESP32, it took a hot second to get the code just right so it wouldn’t send NaN’s on startup and cause Home Assistant to disregard those sensors.

I am aware that the phase measurements are total BS until I have two discrete 9 VAC sources connected to the board from both sides of the split-phase, and probably traces cut. I think the power factor calculation might also be similarly affected.

I think that the calibration is a matter of adjusting the current_cal values while the car is charging at a known amperage? Both for the 30A clamps on its circuit and then getting the 100A clamps to read the same amount, right?

Hi

Yep you need to calibrate each CT using a regular ampmeter aside of the CT. I strongly advise to do calibration with no charge at all connected then half-charge and full-charge. You have to do it for each CT and if you change CT on the input of the ESP you have to recalibrate it :wink:
Also be careful to keep some distances between each CT or they may interfere each other :wink:
sample of my ESPHome code:

  - platform: ct_clamp
    sensor: Input_1
    id: Probe_1
    name: "${Probe_1_name}"
    sample_duration: 200ms
    update_interval: 5s
    filters:
      - calibrate_linear:
          - 0 -> 0
          - 0.06 -> 5.5
          - 0.15 -> 14.4

Vincèn

1 Like

If the output is not perfectly linear use:
method: exact
for better mapping.

1 Like

Isn’t it similar at calibrate_linear that is already in code ?

the default method is least_squares

Thanks. I’ve already got the CT’s physically separated as much as possible.

What I think I understood is that I need to, for each input, unplug from the ESP when the circuit is at a known zero load.

Then measure the AC voltage induced by that load and note what it is.

Then re-do at half-loaded and again at fully-loaded.

Using the numbers I note down, I make that change under each CT clamp.