YAML Best Practice for template sensors (new user)

New user of Home Assistant and ESPHome as of yesterday. Previously many many years of Openhab and used to work on ESP32 IDF based products as a day job.

I have a question about best practice, because what I have come up with based upon documentation and forum posts that I can find, seems wrong.

For context, I have made a home energy monitor, using and ESP32devkit and two current clamps (one on my consumer unit, one on solar output). The whole device only measure current, and assumes voltage is a constant. Based on this we can calculate power, energy, import, export, etc. The only real measurement is current (and voltage, by this construction where one day I might measure this) and this is deterministic based upon update_interval.

However, in all the examples I found, the other calculations are also on update_interval whereas it seems to me like they should be only executed when current has an update?

I notice in the logs that current and energy get executed before the current measurement, so are always (roughly) one time step behind.

Note; I am using the inbuilt energy dashboard as the frontend configured as follows:

  • Grid consumption: “Import Energy”
  • Return to grid: “Export Energy”
  • Solar production: “Solar Energy Used”

Question:
What’s the best practice for calculating a sensor value X (using Y), when sensor value Y gets an update?

substitutions:
  update_time: 10s
  voltage_assumed: 243.7f

esphome:
  name: esphome-energy-mains

esp32:
  board: esp32dev

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

# Enable OTA
ota:

# Enable Homeassistant API
api:

# Enable Logging
logger:

# Enable Timing (dependency from total_daily_energy)
time:
  - platform: homeassistant
    id: homeassistant_time

# Output Hardware
output:
  - platform: gpio
    pin: 14
    id: gpio_led_red
  - platform: gpio
    pin: 12
    id: gpio_led_green
  - platform: gpio
    pin: 13
    id: gpio_led_blue

# Input Sensors
sensor:
  - platform: wifi_signal
    name: WiFi Signal
    update_interval: 60s

  # Source sensor hardware connection
  - platform: adc
    pin: GPIO35
    id: current_clamp_consumer_unit_raw
    attenuation: 11db

  - platform: adc
    pin: GPIO34
    id: current_clamp_solar_raw
    attenuation: 11db

  # Consumer Unit Current Measured to Amps
  - platform: ct_clamp
    id: cu_current_measured
    sensor: current_clamp_consumer_unit_raw
    name: Consumer Unit Current Measured
    sample_duration: 200ms
    update_interval: ${update_time}
    filters:
      - multiply: 5.85
#      - calibrate_linear:
#          - 0.005 -> 0
#          - 0.18176 -> 1.0628
    unit_of_measurement: A

  # Solar Production Sensor to Amps
  - platform: ct_clamp
    id: solar_current_measured
    sensor: current_clamp_solar_raw
    name: Solar Production Current Measured
    sample_duration: 200ms
    update_interval: ${update_time}
    filters:
      - multiply: 5.85
#      - calibrate_linear:
#          - 0 -> 0
#          - 1.25544 -> 7.38
    unit_of_measurement: A

  # Consumer Unit Volts
  - platform: template
    name: Consumer Unit Volts
    id: cuVolts
    lambda: return ${voltage_assumed};
    accuracy_decimals: 2
    unit_of_measurement: V
    device_class: voltage
    update_interval: ${update_time}

  # Consumer Unit Amps
  - platform: template
    name: Consumer Unit Amps
    id: cuAmps
    lambda: return id(current_clamp_consumer_unit_raw).state;
    accuracy_decimals: 2
    unit_of_measurement: A
    device_class: current
    update_interval: ${update_time}

  # Consumer Unit Watts
  - platform: template
    name: Consumer Unit Watts
    id: cuWatts
    lambda: return id(cuVolts).state * id(cuAmps).state ;
    accuracy_decimals: 1
    unit_of_measurement: W
    device_class: power
    update_interval: ${update_time}

  # Consumer Unit Energy
  - platform: total_daily_energy
    name: Consumer Unit Energy
    power_id: cuWatts
    filters:
      - multiply: 0.001
    unit_of_measurement: kWh
    device_class: energy
    state_class: total_increasing

  # Solar Generated Volts
  - platform: template
    name: Solar Volts
    id: solarVolts
    lambda: return ${voltage_assumed};
    accuracy_decimals: 2
    unit_of_measurement: V
    device_class: voltage
    update_interval: ${update_time}

  # Solar Generated Amps (50A Clamp)
  - platform: template
    name: Solar Amps
    id: solarAmps
    lambda: return id(solar_current_measured).state;
    accuracy_decimals: 2
    unit_of_measurement: A
    device_class: current
    update_interval: ${update_time}

  # Solar Generated Watts
  - platform: template
    name: Solar Watts
    id: solarWatts
    lambda: return id(solarVolts).state * id(solarAmps).state ;
    accuracy_decimals: 1
    unit_of_measurement: W
    device_class: power
    update_interval: ${update_time}

  # Import Watts
  - platform: template
    name: Import Watts
    id: importWatts
    lambda: >-
      const float dJ = id(cuWatts).state - id(solarWatts).state;
      return dJ > 0.0f ? dJ : 0.0f;
    accuracy_decimals: 1
    unit_of_measurement: W
    device_class: power
    update_interval: ${update_time}

  # Export Watts
  - platform: template
    name: Export Watts
    id: exportWatts
    lambda: >-
      const float dJ = id(cuWatts).state - id(solarWatts).state;
      return dJ > 0.0f ? 0.0f : abs(dJ);
    accuracy_decimals: 1
    unit_of_measurement: W
    device_class: power
    update_interval: ${update_time}

  # Solar Generated Energy
  - platform: total_daily_energy
    name: Solar Energy
    power_id: solarWatts
    filters:
      - multiply: 0.001
    unit_of_measurement: kWh
    device_class: energy
    state_class: total_increasing

  # Solar Consumed Watts
  - platform: template
    name: Solar Watts Used
    id: solarConsumedWatts
    lambda: >-
      const float solar = id(solarWatts).state;
      const float house = id(cuWatts).state;
      return house > solar ? solar : house;
    accuracy_decimals: 1
    unit_of_measurement: W
    device_class: power
    update_interval: ${update_time}

  # Import Energy
  - platform: total_daily_energy
    name: Import Energy
    power_id: importWatts
    filters:
      - multiply: 0.001
    unit_of_measurement: kWh
    device_class: energy
    state_class: total_increasing

  # Export Energy
  - platform: total_daily_energy
    name: Export Energy
    power_id: exportWatts
    filters:
      - multiply: 0.001
    unit_of_measurement: kWh
    device_class: energy
    state_class: total_increasing

  # Solar Consumed Energy
  - platform: total_daily_energy
    name: Solar Energy Used
    power_id: solarConsumedWatts
    filters:
      - multiply: 0.001
    unit_of_measurement: kWh
    device_class: energy
    state_class: total_increasing

Your use case looks like it is easier handled using the copy platform instead of the template one as you don’t need to worry about the update interval at all then:

Thanks for the reply.

I don’t see it mentioned in the docs, but does this create a new sensor (which I can ass a lambda or something to) or is it purely just a mirror?

So, I have a current clamp sensor which provides amps, and a voltage sensor which provides volts. I think want a power sensor (so it works with the energy dashboard) which is voltage multiplied by current. More specifically, this should only be calculated/updated if voltage or current are updated, rather than on a deterministic loop.

This isn’t a “copy” of voltage nor current.

Apologies if I have misunderstood the “copy”. I just don’t see any example that allows you to modify the data in this copy (e.g. copy of “current”, and multiply it by “voltage”, to make a new variable “power”)

It is a mirror with the same update interval etc. but the idea indeed is that you (can) put different filters/lambdas for your use case.

In that case you will probably go with a template sensor.

To update it when the “source” readings are updated you can make use of sensor.template.publish action on the source sensor.