I run two Tesla PVI-45 inverters on the south face of the roof, and the only piece of the install I’m not totally happy with is that Tesla still doesn’t expose a documented local API for them. There’s an ESP32-style local endpoint on Powerwall hardware, but a stand-alone solar install — inverters, no Powerwall — has to go through Tesla’s cloud account. That’s annoying for the same reason any cloud-only integration is annoying: you’re one rate-limit policy change away from your dashboard going dark.
The good news is that with the tesla_custom HACS integration, Home Assistant’s integration platform, and a small package file, you can build a setup that’s operationally local for everything that matters — daily totals, automations, dashboards — even if the upstream pull from Tesla’s API is technically still cloud. This post is the YAML I actually run, why it’s structured the way it is, and the failure modes I’ve hit.
What you get out of the box from tesla_custom
tesla_custom is the well-maintained community integration that wraps Tesla’s owner-account API. After OAuth, you get entities for each vehicle and each energy site on the account. For a solar-only site (no Powerwall, no Wall Connector charging data) the useful entities boil down to one:
sensor.my_home_solar_power— instantaneous production in watts, updated every couple of minutes via the cloud API.
That’s it. No daily total. No monthly total. No “today’s production” tile. Tesla’s app shows you those numbers, but the API the integration uses doesn’t return them as separate fields — they’re computed client-side in the app from accumulated 5-minute samples.
So the question becomes: how do you get from one instantaneous-watts sensor to a dashboard that’s actually useful?
The integration platform: instantaneous power → cumulative energy
Home Assistant ships with a sensor platform called integration (badly named — it’s a numeric integral, not “integration with another service”). It takes a power sensor and produces an energy sensor by integrating power over time. It’s the standard pattern for any “I have watts, I want kWh” situation, and it works as long as your source sensor updates frequently enough.
sensor:
- platform: integration
source: sensor.my_home_solar_power
name: solar_energy_produced
unique_id: solar_energy_produced
unit_prefix: k
round: 3
method: trapezoidal
A few things worth knowing about this:
method: trapezoidalis what you want for a sensor that’s continuously updating. It assumes power changes linearly between samples, which is roughly true for solar over short intervals. The defaultleftmethod is wrong for this use case — it assumes constant-power-until-next-sample and consistently under-reports.unit_prefix: koutputs kWh instead of Wh. Without it, you’ll be looking at million-Wh numbers in your dashboard by mid-summer.- The output sensor is a
total_increasing-class energy sensor, which is what HA’s Energy Dashboard wants on the “Solar production” line. You can wire this straight into Settings → Dashboards → Energy and you’re done.
The integration sensor’s accuracy depends on your source sensor’s update interval. tesla_custom polls every 2-3 minutes by default. That’s enough to get within a percent or two of Tesla’s own daily total — I’ve spot-checked mine against the Tesla app and the divergence is consistently under 2%. If you tightened the polling, you’d get closer; you’d also burn more API quota and risk getting rate-limited.
Daily / monthly / yearly with utility_meter
The integration sensor accumulates forever. To get rolling daily / monthly / yearly totals, layer utility_meter on top:
utility_meter:
solar_energy_daily:
source: sensor.solar_energy_produced
name: Solar Energy Daily
unique_id: solar_energy_daily
cycle: daily
solar_energy_monthly:
source: sensor.solar_energy_produced
name: Solar Energy Monthly
unique_id: solar_energy_monthly
cycle: monthly
solar_energy_yearly:
source: sensor.solar_energy_produced
name: Solar Energy Yearly
unique_id: solar_energy_yearly
cycle: yearly
utility_meter resets at midnight / month-start / year-start automatically and snapshots the previous period. That gives you sensor.solar_energy_daily, _monthly, _yearly — three sensors that you can drop into a dashboard or use in automations.
Display sensors: kW instead of W
The raw sensor.my_home_solar_power is in watts, which is fine for triggers but ugly on a dashboard tile. A small template sensor cleans it up:
template:
- sensor:
- name: "Solar Power kW"
unique_id: solar_power_kw
unit_of_measurement: "kW"
device_class: power
state_class: measurement
state: >
{{ (states('sensor.my_home_solar_power') | float(0) / 1000) | round(2) }}
availability: >
{{ states('sensor.my_home_solar_power') | is_number }}
- name: "Solar Producing"
unique_id: solar_producing
state: >
{{ states('sensor.my_home_solar_power') | float(0) > 50 }}
Two things here:
- The
availabilitytemplate is the difference between a clean dashboard and one that flashes “unavailable” every time the cloud API hiccups. Without it, when the source sensor goes “unavailable” briefly, the template sensor goes to zero and your daily total briefly drops — which then propagates intoutility_meteras a discontinuity. Withavailability, the template just goes “unavailable” gracefully andutility_meterignores it. Solar Producingis a simple boolean above 50 W. I use it in automations as a condition (“only do X when solar is producing”) and it’s nice not to have to retype the threshold in three places.
Three automations that have actually been useful
These are in automations: inside the same package file. Nothing exotic, but they’re the ones I’ve kept after deleting the half-dozen other “ooh I should automate this” ideas that sounded good and turned out to be noise.
Peak production alert
- id: solar_peak_production_alert
alias: "Solar Peak Production Alert"
trigger:
- platform: numeric_state
entity_id: sensor.my_home_solar_power
above: 5000
condition:
- condition: template
value_template: >
{{ (now() - state_attr('automation.solar_peak_production_alert', 'last_triggered')
| default(now() - timedelta(hours=2), true)).total_seconds() > 3600 }}
action:
- action: notify.mobile_app_charles_phone
data:
title: "Solar Peak"
message: "Solar production hit {{ states('sensor.solar_power_kw') }} kW"
It pings me when production crosses 5 kW. The condition prevents it firing more than once an hour, which matters on a partly-cloudy day when the inverter is bouncing around the threshold. The default fallback (now() - timedelta(hours=2)) handles the edge case where last_triggered is null on a fresh HA restart.
It’s not a critical automation — production crossing 5 kW doesn’t do anything for me. But it’s a nice ambient signal. When it fires, I know the day is going well.
”Solar isn’t producing by 9 AM” alert
- id: solar_no_production_alert
alias: "Solar No Production Alert"
trigger:
- platform: time
at: "09:00:00"
condition:
- condition: numeric_state
entity_id: sensor.my_home_solar_power
below: 50
- condition: sun
after: sunrise
action:
- action: notify.mobile_app_charles_phone
data:
title: "Solar Not Producing"
message: >
Solar panels are showing {{ states('sensor.my_home_solar_power') }}W at 9 AM.
Check inverters or Tesla app for issues.
This one is useful. If the inverters drop off the cloud, the API stops returning data, the sensor goes to zero, and at 9 AM I get a nudge. I’ve gotten this notification three or four times in the past year — twice it was the actual inverter (a soft restart fixed it), once it was a Tesla cloud outage, once it was a wifi issue between the gateway and my router.
The sun: after: sunrise condition matters in winter. December sunrise here is around 7:25 AM, so 9 AM is always after sunrise; in northern latitudes you’d want to bump the trigger time later or use a sun-relative trigger.
Daily production summary at sunset
- id: solar_daily_summary
alias: "Solar Daily Summary"
trigger:
- platform: sun
event: sunset
action:
- action: notify.mobile_app_charles_phone
data:
title: "Solar Daily Report"
message: >
Today's solar production: {{ states('sensor.solar_energy_daily') | round(2) }} kWh
Current month: {{ states('sensor.solar_energy_monthly') | round(1) }} kWh
Sunset trigger — by then the panels are done for the day, but utility_meter hasn’t rolled over yet (that happens at midnight), so solar_energy_daily still holds today’s total. I get a one-line summary on my phone.
Why this is “mostly” local and what would make it fully local
Everything from the integration sensor down — totals, dashboards, automations, alerts — runs on the HA host. If Tesla’s cloud goes dark for an hour, the in-flight automations keep working with the last good sample, and utility_meter doesn’t care.
The piece that’s not local is the upstream pull. tesla_custom hits Tesla’s account API every couple minutes and Tesla can change pricing, deprecate endpoints, or rate-limit at any time.
The fully-local options are limited:
- Wire a CT clamp around the AC output of each inverter and feed it into something like an
iotawattor a Shelly EM. You get instantaneous power locally, no Tesla API involvement. This is the cleanest answer if you’re willing to do the electrical work, and it’s roughly $200-400 in hardware depending on which energy monitor you pick. - Add a Powerwall. The Powerwall has a documented local API that returns solar production directly. Expensive answer to a small problem, but if you’re already considering battery storage it’s worth knowing.
- Run a Span Gen2 or Gen3 (see my Span post) and put the inverter feed on a dedicated panel circuit. Span exposes per-circuit power locally, which gets you close enough.
I’ve been mulling option 1 — a Shelly EM Pro on the inverter feed — and will probably do it eventually. Until then, this tesla_custom + integration + utility_meter stack is the highest leverage-per-line setup I’ve found.
The full package file
Putting all the above into a single packages/solar.yaml keeps it self-contained — drop it in packages/, add homeassistant: packages: !include_dir_named packages to your main config if you haven’t already, and HA picks it up on restart. The whole file is about 100 lines including comments, which is small enough to read in one sitting and modify when you need to tune thresholds.
If you only take one thing from this post, it’s that you should be using integration + utility_meter rather than trying to compute daily totals with template sensors and a last_changed timestamp. I went down the template-sensor path first because it felt clever, and a week later I was deleting it and replacing it with the boring built-in platform that just works.
Boring infrastructure beats clever infrastructure every time.