Mottramlabs / ESP32-4-Channel-Mains-Current-Sensor

ESP32 4-Channel CT mains Power Sensor
18 stars 0 forks source link

ESP Home Code #1

Open talldrin opened 1 year ago

talldrin commented 1 year ago

I just purchased one of these from your ebay store. Do you have the code setup for ESP Home that you could provide? I am planning on using this with home assistant.

swoopgrandchamp commented 10 months ago

The following code is working in a ESP32. Note that my voltage sensor comes from homeassistant, you can use a fixed value if you want or don't have a voltage meter.

substitutions:
  update_time: never
  disp_name: "house-power-sensor"

esphome:
  name: house-power-sensor
  friendly_name: house-power-sensor

esp32:
  board: esp32dev
  framework:
    type: arduino

# Enable logging
logger:
  level: debug

# Enable Home Assistant API
api:
  encryption:
    key: "USE YOUR ENCRYPTION KEY HERE"

ota:

wifi:
  ssid: !secret wifi_ssid
  password: !secret wifi_password
  ap:
    ssid: "house-power-sensor"
    password: "PROVIDE AP PASSWORD"

time:
  - platform: homeassistant
    id: homeassistant_time

interval:
  - interval: 5s
    then:
      - script.execute: readCTs
script:
  - id: readCTs
    then:
      - component.update: ct1Amps
      - delay: 500ms
      - component.update: ct1Watts
      - delay: 500ms
      - component.update: ct2Amps
      - delay: 500ms
      - component.update: ct2Watts
      - delay: 500ms
      - component.update: ct3Amps
      - delay: 500ms
      - component.update: ct3Watts
      - delay: 500ms
      - component.update: ct4Amps
      - delay: 500ms

switch:
   - platform: restart
     name: "Restart"

sensor:
  - platform: ct_clamp
    sensor: adc_sensor_1
    name: ct1Amps
    id: ct1Amps
    update_interval: never
    sample_duration: 200ms
    filters:
      - calibrate_linear:
          - 0 -> 0
          - 0.07972 -> 7.72

  - platform: adc
    pin: GPIO34
    id: adc_sensor_1
    attenuation: 11db
    internal: true

  - platform: ct_clamp
    sensor: adc_sensor_2
    name: ct2Amps
    id: ct2Amps
    update_interval: never
    sample_duration: 200ms
    filters:
      - calibrate_linear:
          - 0 -> 0
          - 0.08278 -> 7.71

  - platform: adc
    pin: GPIO35
    id: adc_sensor_2
    attenuation: 11db
    internal: true

  - platform: ct_clamp
    sensor: adc_sensor_3
    name: ct3Amps
    id: ct3Amps
    update_interval: never
    sample_duration: 200ms
    filters:
      - calibrate_linear:
          - 0 -> 0
          - 0.08317 -> 7.84

  - platform: adc
    pin: GPIO36
    id: adc_sensor_3
    attenuation: 11db
    internal: true

  - platform: ct_clamp
    sensor: adc_sensor_4
    name: ct4Amps
    id: ct4Amps
    update_interval: never
    sample_duration: 200ms
    #filters:
      #- calibrate_linear:
          #- 0 -> 0
          #- 0.08277 -> 7.80

  - platform: adc
    pin: GPIO39
    id: adc_sensor_4
    attenuation: 11db
    internal: true

  - platform: wifi_signal
    name: "wifi_signal"
    update_interval: 1min
  - platform: uptime
    name: "uptime"
    id: Uptime    
    update_interval: 1min

#Watts per channel
  - platform: template
    name: CT1 Watts
    id: ct1Watts
    lambda: return id(ct1Amps).state * id(ic1Volts).state;
    accuracy_decimals: 0
    unit_of_measurement: W
    icon: "mdi:flash-circle"
    update_interval: never

  - platform: template
    name: CT2 Watts
    id: ct2Watts
    lambda: return id(ct2Amps).state * id(ic1Volts).state;
    accuracy_decimals: 0
    unit_of_measurement: W
    icon: "mdi:flash-circle"
    update_interval: never

  - platform: template
    name: CT3 Watts
    id: ct3Watts
    lambda: return id(ct3Amps).state * id(ic1Volts).state;
    accuracy_decimals: 0
    unit_of_measurement: W
    icon: "mdi:flash-circle"
    update_interval: never

  - platform: total_daily_energy
    name: CT1 Total kWh
    power_id: ct1Watts
    filters:
      - multiply: 0.001
    unit_of_measurement: kWh
    icon: "mdi:flash"

  - platform: total_daily_energy
    name: CT2 Total kWh
    power_id: ct2Watts
    filters:
      - multiply: 0.001
    unit_of_measurement: kWh
    icon: "mdi:flash"

  - platform: total_daily_energy
    name: CT3 Total kWh
    power_id: ct3Watts
    filters:
      - multiply: 0.001
    unit_of_measurement: kWh
    icon: "mdi:flash"

  - platform: template
    name: Total Amps
    id: totalAmps
    lambda: return id(ct1Amps).state + id(ct2Amps).state + id(ct3Amps).state ;
    accuracy_decimals: 2
    unit_of_measurement: A
    icon: "mdi:flash"
    update_interval: 15s

  - platform: template
    name: Total Watts
    id: totalWatts
    lambda: return id(totalAmps).state * id(ic1Volts).state;
    accuracy_decimals: 1
    unit_of_measurement: W
    icon: "mdi:flash"
    update_interval: 15s

  - platform: total_daily_energy
    name: Total kWh
    power_id: totalWatts
    filters:
      - multiply: 0.001
    unit_of_measurement: kWh
    icon: "mdi:flash"

  - platform: homeassistant
    name: "Voltage"
    id: ic1Volts
    entity_id: sensor.house_voltage
    internal: true

  - platform: template
    name: Voltage
    id: voltageupdate
    lambda: return id(ic1Volts).state;
    accuracy_decimals: 1
    unit_of_measurement: V
    update_interval: 60s
talldrin commented 10 months ago

Thanks! I will give it try.

Matt2k34 commented 10 months ago

Thanks! I will give it try.

Did this work for you?

The following code is working in a ESP32. Note that my voltage sensor comes from homeassistant, you can use a fixed value if you want or don't have a voltage meter.

Thank you for this! This has got me close, but I'm having issues calibrating via calibrate_linear. I think I'm being dense because I'm not sure what number I should be using.

I'm assuming it should be: [22:49:43][D][sensor:094]: 'ct1Amps': Sending state 0.15046 A with 2 decimals of accuracy This is my zero load state. & [22:50:28][D][sensor:094]: 'ct1Amps': Sending state 0.33105 A with 2 decimals of accuracy this is with a 2kw resistive load.

I believe this means it should be:

(2000/245v - to give me 8.16A) - I'm having issues regardless how I calculate this with being unable to zero, and receiving either negative or huge wattage numbers.

e..g the above now becomes: -1,623W instead of zero.

[22:54:24][D][sensor:094]: 'ct1Amps': Sending state -6.62770 A with 2 decimals of accuracy [22:54:25][D][sensor:094]: 'CT1 Watts': Sending state -1623.78662 W with 0 decimals of accuracy

Any assistance appreciated. I've never done calibrate_linear before and I feel like I'm missing something. I'm using the ESP32 - 30 pin version (devkit v1) and I've tried cutting this down to just the CT1 config to make it easier to read the logs etc, and it was no help.

Any help appreciated. I feel like I'm getting nowhere with the documentation / other examples for calibration

Thanks!

also FYI: Some of the CT4 stuff is missing in your code above :)

PeteBa commented 10 months ago

By way of example, a common SCT013-000 will theoretically generate a 0V signal at the ESP32 gpio pin when there is a 0 amp load connected through the CT. Similarly, the same CT will generate a 1.1V signal at the ESP32 when there is a 100A load. This provides a pretty good starting point for calibrating the ct_clamp platform using the following filter config:

- calibrate_linear:
     method: exact
     datapoints:
        - 0 -> 0
        - 1.1 -> 100

If you want to improve low current readings then you can remove any load wire through the CT and look for the corresponding "RAW AC" line in the ESPHOME log. The number here is the left side of the correlation mapping as it basically represent the voltage at the gpio pin. The right side should be set to zero as that represents the load current.. In essence, we have added a second correlation line that maps low values to zero rather than having them go negative:

[13:18:54][D][ct_clamp:041]: 'Channel 4 Current' - Raw AC Value: 0.001A after 229 different samples (1145 SPS)

- calibrate_linear:
     method: exact
     datapoints:
        - 0 -> 0
        - 0.001 -> 0
        - 1.1 -> 100

If you have a multimeter that can measure resistance values then you can go a further step to improve that last correlation pair. The 1.1 -> 100 mapping assumes that the board is using a 22 ohm resistor to convert the 50mA signal that the CT produces at 100A into a 1.1V signal at the gpio pin. This is based on ohms law that voltage = current x resistance. So 0.05 x 22 is equal to 1.1V. However, the resistor is not generally exactly 22 ohms rather it is nominally within +/-1%. Mine is 22.25 ohms. So I can improve the correlation a little bit by using 1.1125 (i.e. 0.05 x 22.25) as the new left side of the last correlation mapping:

- calibrate_linear:
     method: exact
     datapoints:
        - 0 -> 0
        - 0.001 -> 0
        - 1.1125 -> 100

This gets me to within a few percent of my reference meter that has higher tolerance resistors, internal voltage reference, 12bit ADC, high frequency sampling/smoothing etc. Not bad for a relatively inexpensive board.

I struggle with using things like a 60W bulb or 2kW space heaters to create correlation maps as there are too many wide tolerances involved that can easily make derived calibrations spurious. For a start these loads are at the low end of the CT's range and you would really want something at the high end. But loads of 80-100A are pretty hard to create and measure in the average home. You then have the difficulty of converting watts to amps given that residential voltages can easily vary +/-10% from nominal. But mileage may vary on this.

Hopefully helpful.

substitutions:
  devicename: current-sensor
  friendlyname: Current Sensor

esphome:
  name: ${devicename}
  friendly_name: ${friendlyname}

esp32:
  board: esp32dev
  framework:
    type: arduino

wifi:
  domain: !secret wifi_domain
  ssid: !secret wifi_ssid
  password: !secret wifi_password
  ap:
    ssid: ${devicename}
    password: !secret wifi_ap_password

api:
logger:
ota:

sensor:
  ################ Channel 4 - 100A CT 50mA  ################
  - platform: adc
    pin: GPIO39
    name: Channel 4 ADC
    id: channel_4_adc
    attenuation: 11db
    internal: true

  - platform: ct_clamp
    sensor: channel_4_adc
    name: Channel 4 Current
    id: channel_4_current
    update_interval: 2s
    sample_duration: 200ms
    filters:
      - calibrate_linear:
          method: exact
          datapoints:
            - 0 -> 0
            - 0.001 -> 0           # for CT with non-zero offset
            - 1.1125 -> 100     # For current based CT with 22.25 Ohm burden
      - sliding_window_moving_average:
          window_size: 5
          send_every: 5
          send_first_at: 5
Matt2k34 commented 10 months ago

Hopefully helpful.

100% yes. Thank you very much for this post. It had been driving me crazy and I have (what I think) is a working test bed, and proof that these boards will do exactly what I need (I just need smaller clamps)

Slightly embarrassingly, my issues were:

  1. the CT clamp socket being new, when testing the clamp hadn't seated correctly. I must have been knocking the connection and making/breaking it when playing around which is what sent me into wild positive/negative results. Pushing the CT clamp "3.5mm jack" firmly in gave a very crunchy click - but no weirdness since.
  2. Arrows on my CT seemingly are backwards, rather than pointing to the load they point to the source.

I've configured vs 750w, 1250w, and 2000w settings on a resistive heater and it can get within 18w of it at 750/1250 and within 40w of 2000 - More tweaking to do but i'll end up buying some smaller clamps for lower load testing. I used a multiplier to get near these figures (quick and dirty) but it lets me at least get what I need from the test in prep for a larger purchase of clamps on black friday :)

Plan is to use the large 100a clamp around connectivity between main consumer unit & loft so I can see directionality into/out of the consumer unit at grid and loft space (solar & batteries) - i'll be able to put about 35-38a load in for that configuration so I think that will be ok?

I'll then swap the other inputs to ~20/1V clamps so I can measure lower power / 13a devices.

Honestly, Thank you so much for your help - and for completeness here's (the main sensor section) of the code used. I've pulled in my voltage from HomeAssistant into ESPHome - as the solar battery system has a CT on the mains incoming, I know the incoming voltage pretty accurately. (in the code below, I've used a GROUP (sensor.voltage_group) in Home Assistant, so that if in future I want/need to change the device providing voltage, or I want to average multiple, I can just add the entities in).

It's a shame there's no fleshed out examples on Github / Mottramlabs site - I feel like these boards feel very good quality and they'd sell super well if it was a bit more noob-friendly. (or someone wrote a great guide that didn't assume you knew esphome calibration etc. prior)

Thanks Again! Matt


time:
  - platform: homeassistant
    id: homeassistant_time

switch:
   - platform: restart
     name: "Restart"

sensor:

################ Pull Voltage in from Home Assistant  ################

  - platform: homeassistant
    name: "Voltage"
    id: current_voltage_from_ha
    entity_id: sensor.voltage_group
    internal: true

  - platform: template
    name: Voltage
    id: voltageupdate
    lambda: return id(current_voltage_from_ha).state;
    accuracy_decimals: 1
    unit_of_measurement: V
    update_interval: 1s

################ Channel 1 - 100A CT 50mA  ################
  - platform: adc
    pin: GPIO34
    name: Channel 1 ADC
    id: channel_1_adc
    attenuation: 11db
    internal: true

  - platform: ct_clamp
    sensor: channel_1_adc
    name: Channel 1 Current
    id: channel_1_current
    update_interval: 2s
    sample_duration: 200ms
    filters:
      - multiply: 4.0
      - calibrate_linear:
          method: exact
          datapoints:
            #- 0 -> 0
            - 0.003 -> 0           # for CT with non-zero offset
            - 0.089 -> 2     # For current based CT with 22.25 Ohm burden
      - sliding_window_moving_average:
          window_size: 3
          send_every: 3
          send_first_at: 3

##Watts
  - platform: template
    name: CT1 Watts
    id: channel_1_watts
    lambda: return id(channel_1_current).state * id(current_voltage_from_ha).state;
    accuracy_decimals: 0
    unit_of_measurement: W
    icon: "mdi:flash-circle"
    update_interval: 2s

 ################ Channel 2 - 100A CT 50mA  ################
  - platform: adc
    pin: GPIO35
    name: Channel 2 ADC
    id: channel_2_adc
    attenuation: 11db
    internal: true

  - platform: ct_clamp
    sensor: channel_2_adc
    name: Channel 2 Current
    id: channel_2_current
    update_interval: 2s
    sample_duration: 200ms
    filters:
      #- calibrate_linear:
          #method: exact
          #datapoints:
            #- 0 -> 0
            #- 0.002 -> 0           # for CT with non-zero offset
            #- 1.1115 -> 100     # For current based CT with 22.25 Ohm burden
      - sliding_window_moving_average:
          window_size: 5
          send_every: 5
          send_first_at: 5

##Watts
  - platform: template
    name: CT2 Watts
    id: channel_2_watts
    lambda: return id(channel_2_current).state * id(current_voltage_from_ha).state;
    accuracy_decimals: 0
    unit_of_measurement: W
    icon: "mdi:flash-circle"
    update_interval: never

  ################ Channel 3 - 100A CT 50mA  ################

  - platform: adc
    pin: GPIO36
    name: Channel 3 ADC
    id: channel_3_adc
    attenuation: 11db
    internal: true

  - platform: ct_clamp
    sensor: channel_3_adc
    name: Channel 3 Current
    id: channel_3_current
    update_interval: 2s
    sample_duration: 200ms
    filters:
      #- calibrate_linear:
          #method: exact
          #datapoints:
            #- 0 -> 0
            #- 0.002 -> 0           # for CT with non-zero offset
            #- 3.3325 -> 300     # For current based CT with 22.25 Ohm burden
      - sliding_window_moving_average:
          window_size: 5
          send_every: 5
          send_first_at: 5

##Watts
  - platform: template
    name: CT3 Watts
    id: channel_3_watts
    lambda: return id(channel_3_current).state * id(current_voltage_from_ha).state;
    accuracy_decimals: 0
    unit_of_measurement: W
    icon: "mdi:flash-circle"
    update_interval: never

################ Channel 4 - 100A CT 50mA  ################
  - platform: adc
    pin: GPIO39
    name: Channel 4 ADC
    id: channel_4_adc
    attenuation: 11db
    internal: true

  - platform: ct_clamp
    sensor: channel_4_adc
    name: Channel 4 Current
    id: channel_4_current
    update_interval: 2s
    sample_duration: 200ms
    filters:
      #- calibrate_linear:
          #method: exact
          #datapoints:
            #- 0 -> 0
            #- 0.002 -> 0           # for CT with non-zero offset
            #- 1.1125 -> 100     # For current based CT with 22.25 Ohm burden
      - sliding_window_moving_average:
          window_size: 5
          send_every: 5
          send_first_at: 5

##Watts
  - platform: template
    name: CT4 Watts
    id: channel_4_watts
    lambda: return id(channel_4_current).state * id(current_voltage_from_ha).state;
    accuracy_decimals: 0
    unit_of_measurement: W
    icon: "mdi:flash-circle"
    update_interval: never

############# Total Daily Energy - kWh
  - platform: total_daily_energy
    name: CT1 Total kWh
    power_id: channel_1_watts
    filters:
      - multiply: 0.001
    unit_of_measurement: kWh
    icon: "mdi:flash"

############# Total Amps - A
  - platform: template
    name: Total Amps
    id: totalAmps
    lambda: return id(channel_1_current).state + id(channel_2_current).state + id(channel_3_current).state + id(channel_4_current).state ;
    accuracy_decimals: 2
    unit_of_measurement: A
    icon: "mdi:flash"
    update_interval: 15s

############# Total Watts - W
  - platform: template
    name: Total Watts
    id: totalWatts
    lambda: return id(totalAmps).state * id(current_voltage_from_ha).state;
    accuracy_decimals: 1
    unit_of_measurement: W
    icon: "mdi:flash"
    update_interval: 15s

  - platform: total_daily_energy
    name: Total kWh
    power_id: totalWatts
    filters:
      - multiply: 0.001
    unit_of_measurement: kWh
    icon: "mdi:flash"
PeteBa commented 10 months ago

Matt, glad the comments were useful. I picked up a few things in your response that I thought interesting and raised a couple of questions/follow-ups. Hope that is OK.

You mention that the CT direction labelling is wrong, can I ask how you determined that ? This board alone cannot measure flow direction as you need simultaneous access to voltage phase to achieve that. If indeed the label is wrong then you probably have counterfeit CTs which is unfortunately common through some online marketplaces. If so then I would replace the CTs through a specialist supplier.

I see you are using a standard two element heater to create your calibration maps. Can I ask if you have a reference meter to reliably determine the actual wattage of the unit before trying to calibrate the CT ? I also have a 750/1250/2000W heater but the numbers on the box are very different to the actual wattage of the device which is 870/1,340/2,210. If I used the box numbers to calibrate the CT then I would be building in a ~10% error. I would be better off sticking with the theoretical calibration numbers or alternatively buying a relatively cheap but somewhat accurate "kill-a-watt" style device.

One thing to be aware of, If you are going to use lower value CTs then be very careful to only use them on circuits that are guaranteed to have a maximum current load under the nominal value of the CT. For example, passing a 10A load through a 5A CT will likely fry the ESP32 gpio pin. I would suggest always using a higher value CT than the fuse of the circuit being measured for safety.

For completeness, if using a voltage based CT (rather than the 50mA type above). Then the starting point calibration is even easier. The CT will generate 0V when the load is 0A and 1V when the load is at it's nominal value. So for a 30A CT the config would be:

- calibrate_linear:
     method: exact
     datapoints:
        - 0 -> 0
        - 1 -> 30
Matt2k34 commented 10 months ago

Hope that is OK.

Of course :D feel free, to ask anything ^

If indeed the label is wrong then you probably have counterfeit CTs which is unfortunately common through some online marketplaces.

They were indeed, Cheap & cheerful imports from eBay. I'd have to re-test to see if it makes a difference, but in early testing (albeit with issues) it seemed to. I'll see where I can get them for a decent price that might be a little less grey market. These CTs feel pretty shoddy in comparison to other clamps I've used for other things (e.g. EV charger, Solar battery charger). The "YHDC" print on the side I don't think is overly clear making me think they're potentially none genuine. (unless that's "normal")

Can I ask if you have a reference meter to reliably determine the actual wattage of the unit before trying to calibrate the CT ? Yes - I've got a 3pin "kill-a-watt" style meter which I trust as accurate (or at least accurate enough) - my heater comes in pretty much smack on the money of the 750/1250/2000. I've also tested this against a Shelly 1PM which is "there abouts" on my heater & plug in K-A-W. Although not perfect it's probably "good enough" for my needs.

I'll do some better calibration and setup now I know that "this works" as a solution, this has all been a bit rough and ready as a POC really.

I would suggest always using a higher value CT than the fuse of the circuit being measured for safety.

Yep :) So the thought being 20A/1v CTs will give me enough to cover 13A/220V appliances with plenty of headroom to protect the esp. I'll only use the larger 100A clamps for the cabling between my solar & battery setup, and the main consumer unit (I intend to add extra battery storage meaning it'll be able to push/pull about 48A to/from the main consumer unit under full load - IF I can generate that much usage :) ). Although it's less than "perfect" I'll probably stick to 20A CTs for all "plug in" device monitoring - as anything smaller will likely be fused in the plug at 13A anyway.

Thank you for the example for the voltage CT, this entire thread has been extremely helpful. I found the calibration process infuriating as everyone mentions how to do it - but no one has a "did you check this?" type troubleshooting steps when it doesn't do what you expect.

Joshndroid commented 5 months ago

Sorry everyone for the bump in this. I picked up the 4 Port ESP32-s2 version of the boards... Wemos ESP32-S2 Mini After fighting for a couple days (dead esp32-s2 boards) and ending up needing to bypass the use of esphome web and manually flashing it I at least have a single esp32-s2 board seen by home assistant. I realised that once i plugged it into the 4-port board, it would no longer find the device. Searching around I have found here and realised I would need a bit more code to get this going. I used the example above and slightly changing the top-most section I seem to get an error in relation to the ADC pins. I have tried searching online for the ADC pins and it seems that the supplied GPIO pins above seem to be correct.

` sensor.adc: [source /config/esphome/wemoss2-4-port.yaml:80] platform: adc

ESP32S2 doesn't support ADC on this pin. pin: GPIO34 id: adc_sensor_1 attenuation: 11db internal: True sensor.adc: [source /config/esphome/wemoss2-4-port.yaml:97] platform: adc

ESP32S2 doesn't support ADC on this pin. pin: GPIO35 id: adc_sensor_2 attenuation: 11db internal: True sensor.adc: [source /config/esphome/wemoss2-4-port.yaml:114] platform: adc

ESP32S2 doesn't support ADC on this pin. pin: GPIO36 id: adc_sensor_3 attenuation: 11db internal: True sensor.adc: [source /config/esphome/wemoss2-4-port.yaml:131] platform: adc

ESP32S2 doesn't support ADC on this pin. pin: GPIO39 id: adc_sensor_4 attenuation: 11db internal: True '

Any tips for this eager user to get this to work (I have YHDC sct-13 clamps 100A/50MA to use on the house to replace a pain in the ass efergy monitor

Joshndroid commented 5 months ago

I have managed to work out a few things on getting this going... i Have linked this issue in my own separate notes issue to help others with the style that I have.