Measure mains voltage with ZMPT101B and ADS1115

What would be the most common/straightforward approach to integrate the ZMPT101B sensor into HA (through ADS1115)?

I found this Zmpt101b Precision Voltage Sensor Module - #7 by VdR and Zmpt101b Precision Voltage Sensor Module - #13 by mupsje

The latest looks more complex to me and I’m wondering if there is an easier way to accomplish this (reading VAC).


Hi, the problem is that you need to calibrate first.
Best with a oscilloscope.

I’m also busy with that code :stuck_out_tongue_winking_eye:

The one I made from emonlib is not 100%,
I’m now busy to make one with other library that calibrate and then makes the measurement.
There is no other yet available.

Trust me, I look whole internet and GitHub for it.
I have one with Mqtt ,but also not complete 100%
I will upload it later.

Also I noticed that the measurement with a Arduino or esp8266 is most stable and with power supply directly on the zmtp101b as in my drawing.

Covid is in the house at the moment.
So I need to get better first.


Work in progress, so not 100%

Make the code better and be nice… upload it here. :wink:

I notticed that the measurement is fluctuating to much , so that needs to be better.

//#include <ESP8266WiFi.h>                                       //esp8266
#include <WiFi.h>                                            //esp32
#include <PubSubClient.h>
#include "EmonLib.h"        

#define VOLT_CAL 123  // Calibration vallue
EnergyMonitor emon1;             

const char* ssid  = "#################";
const char* password  = "############";
const char* mqtt_server = "###########";
char* deviceId  = "Slpkm_Yanu_Voltage"; // Name of the sensor
char* stateTopic = "home-assistant/Slpkm_Yanu_Voltage/Voltage"; //name of the Topic
char buf[4]; // Buffer to store the sensor value
int updateInterval = 1000; // Interval in milliseconds

WiFiClient espClient;
PubSubClient client(espClient);
//unsigned long lastMsg = 0;                             //esp8266
long lastMsg = 0;                                      //esp32
char msg[50];
int value = 0;                                         //esp8266

void setup() {


 emon1.voltage(35, VOLT_CAL, 1.7);  
  client.setServer(mqtt_server, 1883);


void setup_wifi() {
  Serial.print("Connecting to ");

  WiFi.begin(ssid, password);

  while (WiFi.status() != WL_CONNECTED) {

  Serial.println("WiFi connected");
  Serial.println("IP address: ");

void callback(char* topic, byte* message, unsigned int length) {
  Serial.print("Message arrived on topic: ");
  Serial.print(". Message: ");
  String messageVolt;
  for (int i = 0; i < length; i++) {
    messageVolt += (char)message[i];

void reconnect(){
  while (!client.connected()) {
    Serial.print("Attempting MQTT connection...");
    // Attempt to connect
    if (client.connect("ESP8266Client")) {
      // Subscribe
    } else {
      Serial.print("failed, rc=");
      Serial.println(" try again in 5 seconds");
      // Wait 5 seconds before retrying
void loop() {
  if (!client.connected()) {

  long now = millis();
  if (now - lastMsg > 2000) {
    lastMsg = now;

  emon1.calcVI(20,2000);         // Calculate all. No.of half wavelengths (crossings), time-out
  float supplyVoltage   = emon1.Vrms; 

    char tempString[8];
    dtostrf(supplyVoltage, 1, 2, tempString);
    Serial.print("Voltage: ");
//    client.publish("sensorDB", tempString);
    client.publish(stateTopic, itoa(supplyVoltage, buf, 10));

Ohh, get better soon :raised_hands:


To answer your first question, I do not think there is a much easier way than the one I used. I agree it is not totally straight forward, but indeed still simpler than the approach by mupsje (niet kwaad bedoeld).

To answer your last question. Yes there is an easier way to read Vac. Use a power meter with an RS485 bus. But these are generally not cheap. I have A Carlo Gavazzi since I installed a charger for my EV, but have not connected it yet to HA. I saw that ESPhome now has an integration for an Eastron SDM modbus energy monitor.

PZEM 004T that’s the one @VdR means!.

Full integration ESPHome.

@VdR , for me, I just want a voltage measure from a old school light dimmer :wink:
So I can dim my new Led-bulbs
(goedkoopste oplossing voor hallogeenspots naar led)

1 Like

It’s realy not fun!

1 Like

What I’d like to understand is the values you came up with for the filters:

- offset: -2.5485   # compensate the offset (calibration)
- multiply: 338   # calculate mains voltage (calibration)

I’m trying to adjust them so that I get the same readings as my volt meter, but not sure what I’m doing :sweat_smile:

The output signal of the Zmpt101b is an attenuated sinewave with an offset of 1/2Vcc (about 2.5V). To be able to get a good RMS reading from this you need to know the offset and the attenuation.

You change the attenuation with the potentiometer on the Zmpt101b. You want it as low as possible (large signal) but high enough that you get a full sinewave signal (not clipped) up to say 260Vrms. To tune that you need to connect an oscilloscope so you can see the signal.

Once you have set that attenuation you can measure the offset voltage on the output with a multimeter by not connecting anything to the input. That’s your off-sett calibration, you must substract the offset, hence the negative number.

Now you can calculate your attenuation, by comparing the Vrms reading from your ESPhome device with the reading from a multimeter. You tweak that number until you get the same reading.

You will be working with mains voltages, be extremely careful please, double check every move you make.

If you do not have an oscilloscope at hand, set the multiplier to a high value (say 360), get the off-set as described above, then tune the attenuation with the potentiometer, comparing the readout of your ESPhome device with your multimeter. I’m assuming here that you are measuring a 240V mains line.


I guess this can be done with an Arduino and the IDE serial plotter. For 220Vac (I live in Argentina) what values should I look for while adjusting the potentiometer? All I know is that I need an almost perfect sinewave, not chopped at its peak.

When you say “readings”, you mean the Vrms value from the previous step? Or something in the range of 200-240Vac?

Thanks so much for your help!

If your Arduino setup (I’m not familiar) samples fast enough to show the sine wave then that’s all you need. And, correct you need a sinewave that is not chopped at its peak. But also leave some room for when the mains voltage is high.

No, that is not what I mean. After you upload the code from HA to your ESPHome device, ESPHome will start showing the data that is sent to HA. The reading it sent for the mains voltage should be the same as what you measure with your meter.

Like below. It shows the measurements it takes from the ads and the mains voltage it calculated. The mains current comes from the current transformer connected to the same node.

See below again my full (except the passwords part) code for note:

# ========================

# i2C bus

  - sda: D2
    scl: D1
    scan: no
    frequency: 400kHz
    id: bus_a

  - address: 0x48
#   continuous_mode: on

# Switches


  # LED 1
  - platform: gpio
    name: "Mains voltage high"
      number: D3
      inverted: no
    id: voltage_high

  # LED 2
  - platform: gpio
    name: "Mains voltage ok"
      number: D4
      inverted: no
    id: voltage_ok

  # LED 3
  - platform: gpio
    name: "Mains voltage low"
      number: D5
      inverted: no
    id: voltage_low

# Sensors


  # Mains voltage sensor
  - platform: ads1115
    name: "Mains voltage"
    unit_of_measurement: "V"
    accuracy_decimals: 0
    icon: "mdi:flash"
    multiplexer: A0_GND
    gain: 6.144
    update_interval: 24ms

      - offset: -2.5475                       # compenstate the offset (value measured at 0V)
      - lambda: return x * x;
      - sliding_window_moving_average:
          window_size: 1250                   # average over 30 seconds
          send_every: 208                     # report every 05 seconds
          send_first_at: 208
      - lambda: return sqrt(x);
      - multiply: 338                         # calculate mains voltage (calibration)     
    id: mains_voltage

      - above: 240
          - switch.turn_on: voltage_high
          - switch.turn_off: voltage_ok
          - switch.turn_off: voltage_low
      - below: 240
        above: 220
          - switch.turn_off: voltage_high
          - switch.turn_on: voltage_ok
          - switch.turn_off: voltage_low
      - below: 220
          - switch.turn_off: voltage_high
          - switch.turn_off: voltage_ok
          - switch.turn_on: voltage_low

  # Mains current sensor
  - platform: ads1115
    name: "Mains current"
    unit_of_measurement: "A"
    accuracy_decimals: 1
    icon: "mdi:gauge"
    multiplexer: 'A2_A3'
    gain: 1.024
    update_interval: 24ms

      - offset: 0                             # compenstate the offset (value measured at 0A)
      - lambda: return x * x;
      - sliding_window_moving_average:
          window_size: 500                    # average over 12 seconds
          send_every: 83                      # report every 02 seconds
          send_first_at: 83
      - lambda: return sqrt(x);
      - multiply: 52.743                      # calculate mains current (0.5 V = 25 A)     
    id: mains_current

  # Mains power sensor
  - platform: template
    name: "Mains power"
    unit_of_measurement: "W"
    accuracy_decimals: 0
    update_interval: 5s                       # send every 5 seconds
    lambda: return id(mains_current).state * id(mains_voltage).state;
    id: mains_power

  # Peak voltage sensor
  - platform: template
    name: "Peak voltage"
    unit_of_measurement: "V"
    accuracy_decimals: 0
    update_interval: 5s
    lambda: return max( id(mains_voltage).state, id(peak_voltage).state );
    id: peak_voltage

  # Bottom voltage sensor
  - platform: template
    name: "Bottom voltage"
    unit_of_measurement: "V"
    accuracy_decimals: 0
    update_interval: 5s
    lambda: return min( id(mains_voltage).state, id(bottom_voltage).state );
    id: bottom_voltage

  # Peak current sensor
  - platform: template
    name: "Peak current"
    unit_of_measurement: "A"
    accuracy_decimals: 1
    update_interval: 5s
    lambda: return max( id(mains_current).state, id(peak_current).state );
    id: peak_current


  # Status
  - platform: status
    name: "Mains Status"

# End
1 Like

Hola José,

Did you get your setup working? I try to achieve the same setup with the 30 pin ESP32 DOIT DevKit v1


After a lot of frustration and failing with many approaches, a lot of consumed and partially then even used knowledge I arrived at this thread. I started the other way round, connecting only the ZMPT101B to the ESP and loading below sketch to the ESP to calibrate the module with the potentiometer according to the live view in the serial console (following e.g. Measure AC voltage with ZMPT101B and ESP8266 12E -

#include <driver/adc.h>
#define Analog_Pin 33

void setup()
  pinMode(Analog_Pin, ANALOG);
  analogSetPinAttenuation(Analog_Pin, ADC_11db);
  delay (1000);

void loop(void)
//  Serial.println(analogRead(Analog_Pin));

float voltage()
  float v= analogRead(Analog_Pin)*(3.3/4095.0); //Measure the voltage
  return v;

Initially I had GPIO25 but I read that ADC2 will be in conflict with WiFi so I changed to ADC1 - GPIO33. I also currently power the ZMPT101B from a second power source because I read several suggestions to get more stable values when separately powered therefore + and - in the picture are connected to the separate power supply and D and G(ND) connected to the ESP.

I tried different sketches and changed the delay values down, but at best I get random values with a small portion of reactions or something unexpected like VAC lowering when mains connected, nothing stable and reproducible. Lastly I tried the esphome ADC platform with even less corresponding values:

[23:38:23][V][adc:100]: 'ADC': Got voltage=0.1030V
[23:38:23][V][sensor:074]: 'ADC': Received new state 0.102993
[23:38:28][V][adc:100]: 'ADC': Got voltage=0.1523V
[23:38:28][V][sensor:074]: 'ADC': Received new state 0.152296
[23:38:33][V][adc:100]: 'ADC': Got voltage=0.1173V
[23:38:33][V][sensor:074]: 'ADC': Received new state 0.117323
[23:38:38][V][adc:100]: 'ADC': Got voltage=0.1027V
[23:38:38][V][sensor:074]: 'ADC': Received new state 0.102691
[23:38:43][V][adc:100]: 'ADC': Got voltage=0.1847V
[23:38:43][V][sensor:074]: 'ADC': Received new state 0.184719
[23:38:48][V][adc:100]: 'ADC': Got voltage=0.1435V
[23:38:48][V][sensor:074]: 'ADC': Received new state 0.143469
--- connect to mains ---
[23:38:53][V][adc:100]: 'ADC': Got voltage=2.3273V
[23:38:53][V][sensor:074]: 'ADC': Received new state 2.327335
[23:38:58][V][adc:100]: 'ADC': Got voltage=2.3683V
[23:38:58][V][sensor:074]: 'ADC': Received new state 2.368295
[23:39:03][V][adc:100]: 'ADC': Got voltage=2.1625V
[23:39:03][V][sensor:074]: 'ADC': Received new state 2.162496
[23:39:08][V][adc:100]: 'ADC': Got voltage=0.0750V
[23:39:08][V][sensor:074]: 'ADC': Received new state 0.075000
[23:39:13][V][adc:100]: 'ADC': Got voltage=0.3812V
[23:39:13][V][sensor:074]: 'ADC': Received new state 0.381151
--- disconnect from mains ---
[23:39:18][V][adc:100]: 'ADC': Got voltage=0.1030V
[23:39:18][V][sensor:074]: 'ADC': Received new state 0.102966
[23:39:23][V][adc:100]: 'ADC': Got voltage=0.1027V
[23:39:23][V][sensor:074]: 'ADC': Received new state 0.102699

I copied basically VdR’s filters and started with his suggestion to calibrate w/o oscilloscope by raising the multiplier to a high value:

- platform: adc
  pin: GPIO33
  name: "ADC"
  update_interval: 5s
  attenuation: auto
    - offset: 0                       # compenstate the offset (value measured at 0V)
    - lambda: return x * x;
    - sliding_window_moving_average:
        window_size: 1250                   # average over 30 seconds
        send_every: 208                     # report every 05 seconds
        send_first_at: 208
    - lambda: return sqrt(x);
    - multiply: 380                         # calculate mains voltage (calibration)     

I measure ~625 - 630 mVAC between D and G (see pic above) when connected to mains which is equivalent to ~231 VAC in mains side and 0.7 mVAC when mains is disconnected.

I also exchanged the ESP and the ZMPT module. When I connect a DC source, e.g. a 1.2V rechargeable and plot it with the serial monitor the values get pretty close to what they should be and stay stable, but with AC it’s just “wild”.

How did you resolve it?

1 Like

Hi Alex!

No, I switched to another project and was about to pick this one again once I finish that one. But will definitely try your path and let you know the outcome.

Thanks a bunch for sharing!

I tried it with an Arduino Nano v3. With a delay of 100 ms between serial plot and sensor read it showed a nice sine wave and I was able to set the potentiometer to get a wave that is not cut off.

I tried to replicate with ESP32, same program, only analogRead(33) instead of (A0). Kauderwelsch, values with spikes sometimes reacting on mains connections and disconnections but not correlating.
I read in the espressif forums to connect a 0.1 uF capacitor to eliminate noise on the ADC.

Maybe the ESP32 is not really good in that, I tried with ESP8266:
Readings with an ESP-201 (ESP8266 ESP-201 module - first impressions | every 100ms:

and after I applied a voltage divider to lower the 3v3 from the module to 1v max for the ESP8266 to read I got a nice clear sine wave, red arrow where I unplugged:

I continued reading many posts here, in the espressif forum and reddit and I would not say I found directly: do not use the ESP32 for analog readings but very often I found something similar to “for the real readings I use ADS1x15 or Arduino”. And the ESP8266 seems to be quite useful for that, too.

I continued, build up a testsetup with the ESP32 and the ZMPT101B only but with the 0.1 µF ceramic capacitor

and with the sketch changed to 99ms delay between Serial.print and proper adaption to 3V3 and 12-bit

// the setup routine runs once when you press reset:
void setup() {
  // initialize serial communication at 9600 bits per second:
// the loop routine runs over and over again forever:
void loop() {
  // read the input on analog pin 0:
  int sensorValue = analogRead(33);
  // Convert the analog reading (which goes from 0 - 1023) to a voltage (0 - 5V):
  float voltage = sensorValue * (3.3 / 4095.0);
  // print out the value you read:

I out of a sudden got a nice sine wave alternating around ~1.68V. I had to play around a bit with the delay, between 97ms and 99ms it looks good, shorter or longer does not print a nice curve and above 100 it gets super squeezed.

So I went back to ESPHome and with this:

- platform: adc
  pin: GPIO33
  name: "Mains Voltage"
  id: mains_voltage
  unit_of_measurement: "V"
  update_interval: 5s
  attenuation: 11db

I get 1/2 Vcc of the ESP32.
[21:59:12][D][sensor:125]: 'Mains Voltage': Sending state 1.69400 V with 2 decimals of accuracy

Constantly between 1.677 - 1.695 V, not jumping up and down or other weird things as seen before. As soon as I plug it in max values between 0.6 and 2.7 V pop up, so it’s time to calculate the RMS over it - this feature request would now come in very handy: AC Voltage with ESP32 and ZMPT101B · Issue #1389 · esphome/feature-requests · GitHub

Did you switch back to the power monitor project already :slight_smile:

So, I got the sensor calibrated with an Arduino Nano. Pretty decent readings and sine wave (using serial plotter). Then connected to ESP (Wemos D1 mini) again and adjusted the offset based on 0Vac (no electricity). I got an average of 2.399343706

So my offset adjustment now looks like this:

- offset: -2.39934370 # compensate the offset (calibration)

What still I need to understand is where the ‘multiply’ value comes from :thinking:

As far as I understand, this is where the 338 in your code comes from, right @VdR ?
- multiply: 338 # calculate mains voltage (calibration)

Does this mean that I should get 0Vac when nothing is connected to the input? Assuming yes, but currently, I get 4~5Vac

That is correct. The 338 is my calibration factor.

To get a zero reading at no input assumes the instrument is linear over the whole range. It won’t be, that is why you calibrate around the voltage you want to measure. Also you would have to short the input to get a real zero reading. Do not worry about a 4 or 5 V reading at no input.

1 Like

Thanks a lot, @VdR and @NODeeJay. I can now tick this item as done from the To-Do list :grinning_face_with_smiling_eyes: