Skip to main content

dracoli.ch

Mo' script, mo' problems

Notes from Finishing my Year-Long Setup of the Emporia Vue 3 with Home Assistant

About a year ago, I stumbled upon the Emporia Vue power monitor. The idea is that you clamp sensors onto the wiring that comes out of your circuit breaker, and the box collects how much power is used at any given time. You can view your energy usage, broken out by circuit over seconds to months. It’s nice!

But… I generally don’t love using proprietary cloud-hosted services, especially for Home IoT stuff. Other than the privacy concerns with sending granular power data (this can be correlated to when you wake up, when you leave the house, when you bathe, etc.) to someone who just doesn’t need to have this, I don’t like being at the mercy of people who might just force an update that makes the ui unusable, monetize your data, or shut it down entirely and leave you with a brick.

They could be the nicest people who never intend to do that, but not being able to host the service yourself and export the data means that you can do nothing but roll over if they change their minds!

This happened to my Neato vacuum :(

So yeah, I have a problem with these devices that have multiple masters. The good news is that the Vue is an Espressif ESP32-based device and is flashable with ESPHome firmware, which report to Home Assistant, making the data flow closed and making all UI concerns your problem keeping the control in your hands. The bad news is that the rough edges frustrated me enough to revert everything back to stock and sit on it until this week.

This isn’t intended to be a complete guide for setup, they’re more of a collection of notes and lessons I learned setting this up. You are assumed to know how to solder, what an ESP is, and have set up Home Assistant.

Circuit Breaker

You’ll follow the Emporia installation guide, which is very good and clear.

Things that were not super obvious to me:

  • The ESPHome config refers to wire colors. You will need to match each phase to the wire color of the voltage sensing/power harness plugged into the side of the monitor box, not the color of the big wires that feed into your breaker. This is very important.
  • If you have a tandem circuit breaker, the clamp sensor on that circuit needs to be clamped around both wires of that breaker/phase.

Compiling ESPHome

You’ll find a lot of configs scattered on the github repo and discussions, and they’re often not complete or slightly outdated. My config includes the bits you need for:

  • Status LEDs
  • Ethernet (uncomment it and comment out the wifi section)
  • Daily Total Energy readouts
  • Instanteous Power readouts

Here’s my config, which I saved as vue3.yaml:

esphome:
  name: emporiavue3
  friendly_name: Vue 3

external_components:
  - source: github://emporia-vue-local/esphome@vue3
    components:
      - emporia_vue

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

# Enable Home Assistant API
api:
  encryption:
    # Encryption key is generated by the new device wizard.
    key: !secret encryption_key

ota:
  # Create a secure password for pushing OTA updates.
  platform: esphome
  password: !secret ota_password


# Enable logging
logger:
  logs:
    # by default, every reading will be printed to the UART, which is very slow
    # This will disable printing the readings but keep other helpful messages
    sensor: INFO

wifi:
  # Wifi credentials are stored securely by new device wizard.
  ssid: !secret wifi_ssid
  password: !secret wifi_password
    # Wifi LED
  on_connect:
    - light.turn_on: wifi_led
  on_disconnect:
    - light.turn_off: wifi_led

#ethernet:
#  type: RTL8201
#  mdc_pin: GPIO32
#  mdio_pin: GPIO33
#  clk_mode: GPIO0_IN

preferences:
  # please also make sure `restore: false` is set on all `platform: total_daily_energy`
  # sensors below.
  flash_write_interval: "48h"

i2c:
  sda: 5
  scl: 18
  scan: false
  frequency: 400kHz
  timeout: 1ms
  id: i2c_a

switch:
  - platform: restart
    name: Restart  

# Configure the two status LEDs on the Emporia Vue v3 case
# https://github.com/emporia-vue-local/esphome/discussions/264#discussioncomment-9788390
light:
  - platform: status_led
    id: wifi_led
    pin:
      number: 2
      ignore_strapping_warning: true
    restore_mode: RESTORE_DEFAULT_ON
  - platform: status_led
    id: ethernet_led
    pin: 4
    restore_mode: RESTORE_DEFAULT_ON

time:
  - platform: sntp
    id: sntp_time
    timezone: <your time zone from https://en.wikipedia.org/wiki/List_of_tz_database_time_zones#List>

# these are called references in YAML. They allow you to reuse
# this configuration in each sensor, while only defining it once
.defaultfilters:
  - &moving_avg
    # we capture a new sample every 0.24 seconds, so the time can
    # be calculated from the number of samples as n * 0.24.
    sliding_window_moving_average:
      # we average over the past 2.88 seconds
      window_size: 24
      # we push a new value every 1.44 seconds
      send_every: 12
  - &invert
    # invert and filter out any values below 0.
    lambda: 'return max(-x, 0.0f);'
  - &pos
    # filter out any values below 0.
    lambda: 'return max(x, 0.0f);'
  - &abs
    # take the absolute value of the value
    lambda: 'return abs(x);'

# Provide diagnostic information about the platform.
# https://gist.github.com/bradsjm/c2562df0e891f26e27191a31d617d471
text_sensor:
  - platform: wifi_info
    ip_address:
      name: "IP Address"
    ssid:
      name: "Connected SSID"
    mac_address:
      name: "MAC Address"


sensor:
  - platform: emporia_vue
    i2c_id: i2c_a
    phases:
      - id: phase_a  # Verify that this specific phase/leg is connected to correct input wire color on device listed below
        input: <RED,BLUE,BLACK>  # Vue device wire color, match to connected wire color on voltage sensing harness
        calibration: 0.0192775  # 0.022 is used as the default as starting point but may need adjusted to ensure accuracy
        # To calculate new calibration value use the formula <in-use calibration value> * <accurate voltage> / <reporting voltage>
        voltage:
          name: "Phase L Voltage"
          filters: [*moving_avg, *pos]
        phase_angle:
          name: "Phase L Phase Angle"
          filters: [*moving_avg, *pos]
      - id: phase_b  # Verify that this specific phase/leg is connected to correct input wire color on device listed below
        input: <RED,BLUE,BLACK>  # Vue device wire color, match to connected wire color on voltage sensing harness
        calibration: 0.0192775  # 0.022 is used as the default as starting point but may need adjusted to ensure accuracy
        # To calculate new calibration value use the formula <in-use calibration value> * <accurate voltage> / <reporting voltage>
        voltage:
          name: "Phase R Voltage"
          filters: [*moving_avg, *pos]

    ct_clamps:
      - phase_id: phase_a
        input: "A"  # Verify the CT going to this device input also matches the phase/leg
        power:
          name: "Phase L Power"
          id: phase_a_power
          device_class: power
          filters: [*moving_avg, *pos]
      - phase_id: phase_b
        input: "B"  # Verify the CT going to this device input also matches the phase/leg
        power:
          name: "Phase R Power"
          id: phase_b_power
          device_class: power
          filters: [*moving_avg, *pos]
      # Pay close attention to set the phase_id for each breaker by matching it to the phase/leg it connects to in the panel
      - { phase_id: phase_a, input:  "1", power: { id:  cir1, filters: [ *moving_avg, *pos ] } }
      - { phase_id: phase_b, input:  "2", power: { id:  cir2, filters: [ *moving_avg, *pos ] } }
      - { phase_id: phase_a, input:  "3", power: { id:  cir3, filters: [ *moving_avg, *pos ] } }
      - { phase_id: phase_b, input:  "4", power: { id:  cir4, filters: [ *moving_avg, *pos ] } }
      - { phase_id: phase_a, input:  "5", power: { id:  cir5, filters: [ *moving_avg, *pos ] } }
      - { phase_id: phase_b, input:  "6", power: { id:  cir6, filters: [ *moving_avg, *pos ] } }
      - { phase_id: phase_a, input:  "7", power: { id:  cir7, filters: [ *moving_avg, *pos ] } }
      - { phase_id: phase_b, input:  "8", power: { id:  cir8, filters: [ *moving_avg, *pos ] } }
      - { phase_id: phase_b, input:  "9", power: { id:  cir9, filters: [ *moving_avg, *pos ] } }
      - { phase_id: phase_a, input: "10", power: { id: cir10, filters: [ *moving_avg, *pos ] } }
      - { phase_id: phase_b, input: "11", power: { id: cir11, filters: [ *moving_avg, *pos ] } }
      - { phase_id: phase_a, input: "12", power: { id: cir12, filters: [ *moving_avg, *pos ] } }
      - { phase_id: phase_b, input: "13", power: { id: cir13, filters: [ *moving_avg, *pos ] } }
      - { phase_id: phase_a, input: "14", power: { id: cir14, filters: [ *moving_avg, *pos ] } }
      - { phase_id: phase_b, input: "15", power: { id: cir15, filters: [ *moving_avg, *pos ] } }
      - { phase_id: phase_a, input: "16", power: { id: cir16, filters: [ *moving_avg, *pos ] } }
  - { platform: copy, name:  "Circuit 1 Power", source_id:  cir1, filters: *moving_avg }
  - { platform: copy, name:  "Circuit 2 Power", source_id:  cir2, filters: *moving_avg }
  - { platform: copy, name:  "Circuit 3 Power", source_id:  cir3, filters: *moving_avg }
  - { platform: copy, name:  "Circuit 4 Power", source_id:  cir4, filters: *moving_avg }
  - { platform: copy, name:  "Circuit 5 Power", source_id:  cir5, filters: *moving_avg }
  - { platform: copy, name:  "Circuit 6 Power", source_id:  cir6, filters: *moving_avg }
  - { platform: copy, name:  "Circuit 7 Power", source_id:  cir7, filters: *moving_avg }
  - { platform: copy, name:  "Circuit 8 Power", source_id:  cir8, filters: *moving_avg }
  - { platform: copy, name:  "Circuit 9 Power", source_id:  cir9, filters: *moving_avg }
  - { platform: copy, name: "Circuit 10 Power", source_id: cir10, filters: *moving_avg }
  - { platform: copy, name: "Circuit 11 Power", source_id: cir11, filters: *moving_avg }
  - { platform: copy, name: "Circuit 12 Power", source_id: cir12, filters: *moving_avg }
  - { platform: copy, name: "Circuit 13 Power", source_id: cir13, filters: *moving_avg }
  - { platform: copy, name: "Circuit 14 Power", source_id: cir14, filters: *moving_avg }
  - { platform: copy, name: "Circuit 15 Power", source_id: cir15, filters: *moving_avg }
  - { platform: copy, name: "Circuit 16 Power", source_id: cir16, filters: *moving_avg }
  - platform: template
    name: "Total Power"
    lambda: return id(phase_a_power).state + id(phase_b_power).state;
    update_interval: 10s
    id: total_power
    unit_of_measurement: "W"
    device_class: power
    state_class: measurement
  - platform: total_daily_energy
    name: "Total Daily Energy"
    power_id: total_power
    accuracy_decimals: 0
    unit_of_measurement: "Wh"
  - { power_id:  cir1, platform: total_daily_energy, accuracy_decimals: 0, name:  "Circuit 1 Daily Energy" }
  - { power_id:  cir2, platform: total_daily_energy, accuracy_decimals: 0, name:  "Circuit 2 Daily Energy" }
  - { power_id:  cir3, platform: total_daily_energy, accuracy_decimals: 0, name:  "Circuit 3 Daily Energy" }
  - { power_id:  cir4, platform: total_daily_energy, accuracy_decimals: 0, name:  "Circuit 4 Daily Energy" }
  - { power_id:  cir5, platform: total_daily_energy, accuracy_decimals: 0, name:  "Circuit 5 Daily Energy" }
  - { power_id:  cir6, platform: total_daily_energy, accuracy_decimals: 0, name:  "Circuit 6 Daily Energy" }
  - { power_id:  cir7, platform: total_daily_energy, accuracy_decimals: 0, name:  "Circuit 7 Daily Energy" }
  - { power_id:  cir8, platform: total_daily_energy, accuracy_decimals: 0, name:  "Circuit 8 Daily Energy" }
  - { power_id:  cir9, platform: total_daily_energy, accuracy_decimals: 0, name:  "Circuit 9 Daily Energy" }
  - { power_id: cir10, platform: total_daily_energy, accuracy_decimals: 0, name: "Circuit 10 Daily Energy" }
  - { power_id: cir11, platform: total_daily_energy, accuracy_decimals: 0, name: "Circuit 11 Daily Energy" }
  - { power_id: cir12, platform: total_daily_energy, accuracy_decimals: 0, name: "Circuit 12 Daily Energy" }
  - { power_id: cir13, platform: total_daily_energy, accuracy_decimals: 0, name: "Circuit 13 Daily Energy" }
  - { power_id: cir14, platform: total_daily_energy, accuracy_decimals: 0, name: "Circuit 14 Daily Energy" }
  - { power_id: cir15, platform: total_daily_energy, accuracy_decimals: 0, name: "Circuit 15 Daily Energy" }
  - { power_id: cir16, platform: total_daily_energy, accuracy_decimals: 0, name: "Circuit 16 Daily Energy" }

It refers to a secrets.yaml which you will need to populate with the following secrets:

ota_password: <pick a nice, long, secure password>
encryption_key: <https://esphome.io/components/api.html#configuration-variables>

wifi_ssid: <ssid>
wifi_password: <password>

Save both of these files in the same directory.

Original config credit goes to Digiblur’s guide.

Wiring for Flashing

  • You can wire it up with just male-male Dupont jumpers.
    • Solder a wire for ground to the joint labeled TPGND.
    • Solder another wire to the big tab on U4 to supply 3.3V.
    • Solder a wire into the big hole under TP4 (this is GPIO0).
    • Do not attempt to solder directly to the small test pads. You will likely lift the pad like I did.
  • You’ll have to hold two jumpers steady on the RXD (TP3) and TXD (TP4) pads for a few minutes while you’re uploading the new firmware.

Vue3 circuit board with jumpers soldered

  • Connect these jumpers to the Vue3 circuit board using the jumpers you soldered.
  • If you are using an ESP-01 programmer, here is the pinout you need:

ESP-01 programmer pinout

Mine only had the female headers, but the pinout should be the same as the male in the picture.

Uploading with esptool.py

In case you want to use esptool.py to upload the firmware like I did, here’s what I needed for it:

  • Make sure you put the ESP into bootloader mode by touching the jumper wire on GPIO0 to ground at the start of boot.
  • To back up the original firmware:
    • esptool -b 921600 -p <PORT> read_flash 0 0x800000 flash_contents.bin
  • To compile the new firmware:
    • esphome compile vue3.yaml
  • Grab firmware.factory.bin from the esphome folder (mine was in .esphome/build/emporiavue3/.pioenvs/emporiavue3/firmware.factory.bin)
  • To write the new firmware:
    • esptool -p <PORT> write_flash 0x0 <firmware_bin_file>
  • After your Vue3 connects back to wifi, you should be able to upload firmware changes through OTA by running:
    • esphome run vue3.yaml

Home Assistant Configuration

  • HA should find your newly flashed Vue through mDNS. Just input the encryption_key from your secrets.yaml to set up.
  • Note that the total daily energy counters can initialize kind of funny. Every circuit on phase B for me started the first day with near 200kWh values, but these cleared at the start of the next day. Don’t freak out about total energy readings when you first set it up, as long as the total and circuit power readings look fine.

Combining Circuits

Your larger appliances are likely on multiple phases. To aggregate them, you’ll need to use a HA template helper for the sensors across the circuits.

  • The sensor values might be numerical states, but states are strings in HA. If you add them, make sure to use |float to convert them back into numbers, or else addition will cause them to be concatenated as strings into a really large number.
  • For example, an appliance that uses two phases for power might have a template as follows:
{{ states('sensor.emporiavue3_circuit_1_power')|float + states('sensor.emporiavue3_circuit_2_power')|float }}
  • Similarly, you can configure a template helper for total daily power of that same applicance using:
{{ states('sensor.emporiavue3_circuit_1_total_daily_energy')|float + states('sensor.emporiavue3_circuit_2_total_daily_energy')|float }}

Automation Triggers

My washing machine is on its own dedicated circuit. This means that the dropoff of power usage on that circuit indicates a wash cycle has finished. You can combine this in a HA automation to get notifications about cycle completion, even if your machine is an older, non wifi-connected one like mine.

  • Use a threshold helper with an upper limit of some watts (I used 20W) on the washer’s circuit to signal the washer is running, and then trigger an automation to send a notification when that helper’s state changes from on to off.

Dryers may modulate the temperature using a duty cycle, meaning the power will dip down to near 0 multiple times during the drying cycle. This would cause a lot of false positive notifications towards the end of the cycle, but there is a way to deal with this.

  • Set up a threshold helper and automation as for the dryer above, but use a for: time option in the automation that the state must be maintained.

Many thanks to the ESPHome community who contributed code and instructions. I like my new old toy!