Automatically charge your EV when the electrical grid is cleanest. This Home Assistant integration monitors real-time CO₂ intensity, computes a rolling statistical signal, and controls your charger — no YAML automation required.
Most carbon-aware charging tools use static thresholds (e.g., "charge when CO₂ < 200 g/kWh"). This integration uses a Z-score — a measure of how unusual the current CO₂ level is relative to the past 7 days:
Z-score = (current CO₂ − 7-day mean) / 7-day standard deviation
A negative Z-score means the grid is cleaner than usual. A positive Z-score means dirtier than usual. This approach adapts automatically to your local grid's baseline — it works equally well in a region averaging 50 g/kWh or 500 g/kWh.
The charger turns on when:
- The Z-score is below your chosen sensitivity threshold, and
- Fossil fuel generation is below 75% (a hard safety floor)
If carbon data is unavailable or the grid is consistently dirty, built-in fallback windows ensure your car is still charged.
- Fully UI-configured — no YAML editing needed after installation
- Statistical carbon signal — Z-score adapts to your grid's typical range
- Three sensitivity modes — from aggressive carbon avoidance to charging most of the time
- Configurable fallback windows — guaranteed charging windows (overnight and midday by default) on dirty-grid days, each independently adjustable and enable/disable-able
- Roadtrip prep charging — calendar-driven: parses events with a configurable prefix to activate full-speed charging ahead of a trip, with optional SoC targeting and charge limit control
- Departure-day prep charging — weekly schedule so your car is always ready on commute days
- Hysteresis — prevents rapid on/off switching when hovering near the threshold
- Dwell and cooldown — minimum 15-minute on-time and 10-minute off-time to protect the charger
- Dry-run mode — log decisions without touching the charger, for safe testing
- Optional LED indicator — RGB light shows charging state at a glance
- Optional push notifications — get notified when charging starts or stops
- Persisted history — rolling statistics survive Home Assistant restarts
Before installing, you need:
-
A CO₂ intensity sensor — the Electricity Maps integration provides both a CO₂ intensity sensor and a fossil fuel percentage sensor out of the box. Any sensor exposing a numeric CO₂ value (g/kWh) and a fossil fuel percentage will work.
-
A controllable charger switch — your charger must be exposed as a
switchentity in Home Assistant. This integration has been tested with Emporia chargers. Other chargers should work as long as they present a switch entity and expose an attribute indicating whether a car is plugged in. -
Home Assistant 2024.1 or later
- Open HACS in your Home Assistant sidebar.
- Click the three-dot menu (⋮) in the top right and choose Custom repositories.
- Enter the repository URL:
Set the category to Integration, then click Add.
https://github.com/andrewreaganm/carbon-aware-ev-charging - Search for Carbon-Aware EV Charging in HACS and click Download.
- Restart Home Assistant.
- Download or clone this repository.
- Copy the
custom_components/carbon_aware_ev_chargingfolder into your Home Assistantconfig/custom_components/directory. - Restart Home Assistant.
After installation, go to Settings → Devices & Services → Add Integration and search for Carbon-Aware EV Charging. The setup wizard walks you through three steps.
| Field | Description |
|---|---|
| CO₂ Intensity Sensor | Entity providing real-time CO₂ intensity in g/kWh (e.g., sensor.my_region_co2_intensity from Electricity Maps) |
| Fossil Fuel % Sensor | Entity providing the current fossil fuel generation percentage (e.g., sensor.my_region_grid_fossil_fuel_percentage) |
| Charger Switch | The switch entity that controls your EV charger |
| Connection Attribute | The charger switch attribute used to detect if a car is plugged in (default: icon_name) |
| Not-Connected Value | The attribute value when no car is connected (default: CarNotConnected) |
| Charger Power Sensor (optional) | Sensor reporting current charge power in watts; used to populate the charge rate entity |
Emporia users: The connection attribute
icon_namewith valueCarNotConnectedare the correct defaults for Emporia chargers. No changes needed.
These values are fixed after initial setup. To change them, remove the integration and re-add it.
If you have an RGB light (e.g., a smart bulb or LED strip in the garage), you can configure it here for visual status feedback.
| Field | Description |
|---|---|
| RGB Indicator Light | A light entity supporting HS colour |
| LED Effect Selector | A select entity for choosing the light's animation effect |
See LED Indicator for colour meanings. Leave both fields blank to skip LED feedback entirely.
All preferences can be changed later via Settings → Devices & Services → Carbon-Aware EV Charging → Configure, without re-running the full wizard. They are also exposed as entities you can control from a dashboard.
| Field | Description | Default |
|---|---|---|
| Carbon Sensitivity | How strictly to follow carbon signals (Lenient, Moderate, Strict) |
Moderate |
| Departure Hour | Hour of day (0–23) to end departure-prep charging (prep starts 3 hours before) | 5 |
| Departure Days | Days of the week to activate departure prep | Mon–Fri |
| Fallback Window 1 | Overnight charging window start/end hours | 22:00–06:00 (enabled) |
| Fallback Window 2 | Midday charging window start/end hours | 11:00–15:00 (enabled) |
| Dry Run | Log decisions only; do not control the charger | Off |
| Notification Service (optional) | HA notify service (e.g., notify.mobile_app_my_phone) |
— |
| Roadtrip Calendar(s) (optional) | Calendar entities to scan for trip prep events | — |
| Roadtrip Event Prefix (optional) | Prefix identifying roadtrip events (e.g., EV) |
— |
| Default Roadtrip Lead Time | Hours of prep charging before a trip (when not specified in the event) | 3 |
| SoC Sensor (optional) | Sensor reporting the car's current state of charge (%) | — |
| Charge Limit Entity (optional) | number or select entity to set the charge limit for roadtrip prep |
— |
| Mode | Z-score Threshold | Approximate Charge Frequency |
|---|---|---|
| Lenient | < 0.92 σ | ~82% of hours — skips only the dirtiest peaks |
| Moderate | < 0.47 σ | ~68% of hours — avoids above-average carbon periods |
| Strict | < −0.18 σ | ~43% of hours — only charges during genuinely clean windows |
All modes also enforce a 75% fossil fuel hard floor — if more than three-quarters of grid generation is from fossil fuels, the carbon gate will not open regardless of the Z-score.
When the charger is already on, 0.4 σ of hysteresis is added to the threshold before the charger will be turned off. This prevents rapid cycling when the signal is hovering near the boundary.
Which mode should I use?
- Start with Moderate. If your car is frequently not charged enough, switch to Lenient.
- Use Strict if you have generous charging time available and want maximum carbon reduction.
Every 5 minutes (and immediately on relevant state changes) the integration evaluates the following priority chain. The first matching condition wins.
| Priority | Condition | Status | Charger |
|---|---|---|---|
| 1 | Charge mode set to force_off |
Forced Off | Off |
| 2 | Charge mode set to force_on |
Override | On |
| 3 | Carbon gate open (Z-score below threshold, fossil < 75%) | Low Carbon | On |
| 4 | Roadtrip prep window active (and SoC target not yet met) | Roadtrip Prep | On |
| 5 | Departure prep window active | Departure Prep | On |
| 6 | Carbon data unavailable and inside a fallback window | Fallback | On |
| 7 | Carbon data stale (> 60 min, 3 consecutive polls) | Data Stale | Off |
| 8 | Carbon data unavailable | Waiting for Data | Off |
| 9 | Fossil fuel % ≥ 75% | Fossil High | Off |
| 10 | Z-score above threshold | Grid Dirty | Off |
If the car is not connected, the charger is always off regardless of the above — but the integration continues to evaluate and display what it would do if the car were connected.
Fallback windows only activate when carbon data is unavailable. They do not override a good or bad grid signal during normal operation.
Default windows (both configurable and independently toggleable):
- Overnight: 22:00–06:00
- Midday: 11:00–15:00
A 3-hour window before the configured departure hour activates on each selected day. For example, with a departure hour of 5, prep charging runs from 02:00–05:00 on the selected days.
Departure prep is lower priority than the carbon gate — if the grid is clean, the charger runs under the Low Carbon status instead.
If you configure a calendar and event prefix, the integration scans your calendar every 5 minutes for upcoming events. Any event whose title contains [PREFIX ...] triggers prep charging ahead of the trip.
[PREFIX optional_soc% optional_leadh]
Examples:
Road trip [EV 90% 4h]— charge to 90% SoC, start prep 4 hours before departureDrive to airport [EV 80%]— charge to 80%, use the default lead timeLong drive [EV 6h]— no SoC target, start prep 6 hours beforeDay trip [EV]— no SoC target, use the default lead time
If a SoC sensor is configured and the event specifies a SoC target, prep charging stops automatically once the target is reached.
If a charge limit entity is configured, the limit is set to the event's SoC target when prep charging begins.
The integration creates a single device with the following entities:
| Entity suffix | Description |
|---|---|
co2_z_score |
Current Z-score (σ). Negative = cleaner than usual. Attributes: mean_7d, stdev_7d, mean_30d, stdev_30d, co2 |
ev_charging_status |
Current charging decision status (see priority table above). Attributes: status_reason, predicted_state, should_charge, z_score, fossil_pct |
ev_charge_rate_kw |
Current charge power in kW (only created if a power sensor is configured) |
ev_charge_current |
Current charge current in amps (read from the charger switch's charging_rate attribute) |
ev_roadtrip_event |
Timestamp of the next roadtrip prep start time when a prep window is active; unavailable otherwise. Attributes: summary, soc_target, lead_hours, event_start |
| Entity suffix | Description |
|---|---|
ev_low_carbon_now |
On when the carbon gate is open (Z-score below threshold and fossil % below 75%), regardless of car connection |
ev_connected |
On when a car is plugged in |
| Entity suffix | Options | Description |
|---|---|---|
ev_charge_mode |
auto / force_on / force_off |
Override the charging decision |
ev_carbon_mode |
Lenient / Moderate / Strict |
Carbon sensitivity level |
| Entity suffix | Range | Description |
|---|---|---|
ev_departure_hour |
0–23 | Hour at which departure-prep charging ends |
fallback_window_1_start |
0–23 | Fallback window 1 start hour |
fallback_window_1_end |
0–23 | Fallback window 1 end hour |
fallback_window_2_start |
0–23 | Fallback window 2 start hour |
fallback_window_2_end |
0–23 | Fallback window 2 end hour |
| Entity suffix | Description |
|---|---|
fallback_window_1_enabled |
Enable/disable fallback window 1 |
fallback_window_2_enabled |
Enable/disable fallback window 2 |
dry_run |
Enable/disable dry-run mode |
If configured, the RGB light reflects the current grid state (what would happen if the car were connected):
| State | Colour | Effect (car connected) | Effect (car disconnected) |
|---|---|---|---|
| Low Carbon | Green | Rising | Slow Blink |
| Override | Amber | Rising | Slow Blink |
| Roadtrip Prep | Cyan | Rising | Slow Blink |
| Fallback / Departure Prep | Red | Rising | Slow Blink |
| Paused | Red | Slow Blink | Slow Blink |
The LED is only updated when its state actually changes, to avoid unnecessary service calls.
Enable the EV Dry Run switch (or toggle it in integration options) to have the integration evaluate all logic and log its decisions without actually switching the charger, updating the LED, or sending notifications. Check the Home Assistant logs (filter by carbon_aware_ev_charging) to see the decision on each cycle. This is useful for validating configuration before going live.
A ready-made dashboard is included in ev_dashboard.yaml. It provides status glances, Z-score and CO₂ gauges, 48-hour history graphs, an optional Plotly charge-window overlay, and 30-day trend cards.
To install: Go to Settings → Dashboards → Add Dashboard, choose "From YAML", and paste the contents of the file. Replace the three placeholder entity IDs (YOUR_CO2_SENSOR, YOUR_FOSSIL_SENSOR, YOUR_CHARGER_SWITCH) with your own entities.
Dashboard YAML
# EV Charging Dashboard — Carbon-Aware EV Charging Integration
# ─────────────────────────────────────────────────────────────────────────────
# BEFORE USING: Replace the three placeholder entity IDs below with your own.
# 1. YOUR_CO2_SENSOR → your CO₂ intensity sensor
# 2. YOUR_FOSSIL_SENSOR → your fossil fuel % sensor
# 3. YOUR_CHARGER_SWITCH → your charger switch entity
#
# To install: Settings → Dashboards → Add Dashboard → choose "From YAML",
# then paste this file's contents.
# ─────────────────────────────────────────────────────────────────────────────
title: EV Charging
views:
- title: Overview
path: ev
icon: mdi:ev-station
cards:
# ── Current Status ────────────────────────────────────────────────────
- type: glance
title: Current Status
show_state: true
entities:
- entity: select.ev_charge_mode
name: Mode
- entity: binary_sensor.ev_connected
name: Car
- entity: YOUR_CHARGER_SWITCH # ← replace with your charger switch
name: Charger
- entity: binary_sensor.ev_low_carbon_now
name: Carbon OK
icon: mdi:leaf
# ── Charging Session ─────────────────────────────────────────────────
- type: glance
title: Charging Session
show_state: true
entities:
# Remove ev_charge_rate_kw if you did not configure a power sensor.
- entity: sensor.ev_charge_rate_kw
name: Charge Rate
icon: mdi:lightning-bolt
- entity: sensor.ev_charge_current
name: Amps
icon: mdi:current-ac
# ── Carbon Signal Gauges ─────────────────────────────────────────────
- type: vertical-stack
cards:
- type: gauge
title: CO2 Z-Score
entity: sensor.co2_z_score
min: -3
max: 3
needle: true
severity:
green: -3 # cleaner than Strict threshold (-0.18σ)
yellow: -0.18 # Strict–Moderate band
red: 0.92 # above Lenient threshold
- type: gauge
title: CO2 Intensity
entity: YOUR_CO2_SENSOR # ← replace with your CO₂ sensor
unit: gCO₂/kWh
min: 0
max: 600
needle: true
severity:
green: 0
yellow: 250
red: 400
- type: gauge
title: Fossil Fuel %
entity: YOUR_FOSSIL_SENSOR # ← replace with your fossil % sensor
unit: "%"
min: 0
max: 100
needle: true
severity:
green: 0
yellow: 50
red: 75
# ── CO2 Intensity History ────────────────────────────────────────────
- type: history-graph
title: CO2 Intensity (48h)
hours_to_show: 48
entities:
- entity: YOUR_CO2_SENSOR # ← replace with your CO₂ sensor
name: CO2 Intensity
# ── Z-Score History ──────────────────────────────────────────────────
- type: history-graph
title: CO2 Z-Score (48h)
hours_to_show: 48
entities:
- entity: sensor.co2_z_score
name: Z-Score
# ── Charge Window History (optional — requires custom:plotly-graph) ──
# Install plotly-graph from HACS Frontend to use this card.
# Green fill = carbon gate open, blue = Z-score, orange = charger on/off.
- type: custom:plotly-graph
title: Charge Windows (48h)
hours_to_show: 48
refresh_interval: 300
entities:
- entity: binary_sensor.ev_low_carbon_now
name: Carbon OK
yaxis: y2
fill: tozeroy
fillcolor: "rgba(0,200,80,0.15)"
line:
color: "rgba(0,200,80,0.4)"
width: 1
- entity: sensor.co2_z_score
name: Z-Score
yaxis: y
line:
color: "rgba(60,120,220,0.9)"
width: 2
- entity: YOUR_CHARGER_SWITCH # ← replace with your charger switch
name: Charger
yaxis: y2
line:
color: "rgba(255,160,0,0.85)"
width: 2
layout:
yaxis:
range: [-3, 3]
title: Z-Score (σ)
yaxis2:
range: [0, 1.5]
overlaying: y
side: right
showgrid: false
showticklabels: false
shapes:
- type: line
x0: 0
x1: 1
xref: paper
y0: -0.18
y1: -0.18
line:
color: "rgba(50,200,80,0.6)"
width: 1
dash: dot
- type: line
x0: 0
x1: 1
xref: paper
y0: 0.47
y1: 0.47
line:
color: "rgba(255,160,0,0.6)"
width: 1
dash: dot
- type: line
x0: 0
x1: 1
xref: paper
y0: 0.92
y1: 0.92
line:
color: "rgba(220,60,60,0.6)"
width: 1
dash: dot
# ── 30-Day CO2 Trend ─────────────────────────────────────────────────
- type: statistics-graph
title: CO2 Intensity – 30 Day Trend
days_to_show: 30
period: day
stat_types:
- mean
- min
- max
entities:
- entity: YOUR_CO2_SENSOR # ← replace with your CO₂ sensor
name: CO2 Intensity
# ── 30-Day Z-Score Trend ─────────────────────────────────────────────
- type: statistics-graph
title: CO2 Z-Score – 30 Day Trend
days_to_show: 30
period: day
stat_types:
- mean
- min
- max
entities:
- entity: sensor.co2_z_score
name: Z-Score
# ── Controls ─────────────────────────────────────────────────────────
- type: entities
title: Controls
entities:
- entity: select.ev_charge_mode
name: Charge Mode
- entity: select.ev_carbon_mode
name: Carbon Sensitivity
- entity: number.ev_departure_hour
name: Departure Prep Hour
- entity: switch.fallback_window_1_enabled
name: Overnight Fallback
- entity: switch.fallback_window_2_enabled
name: Midday Fallback
- entity: switch.dry_run
name: Dry RunThe Z-score requires approximately 7 days of CO₂ data before it becomes fully meaningful. During this warmup period:
- The Z-score is computed as soon as 2 readings exist in the rolling window, but with very few data points the mean and standard deviation are not yet representative.
- When all readings are identical (stdev = 0), the Z-score is reported as
0.0(exactly at the mean). - The carbon gate defaults to
Falsewhile the Z-score is unavailable, so charging falls back to the scheduled windows. - On first install the integration automatically backfills up to 30 days of CO₂ history from the Home Assistant Recorder.
- Rolling history is persisted through restarts, so the integration does not re-warm from scratch after a reboot.
The charger isn't turning on even on clean-grid days.
- Check that
ev_connectedison. If not, verify the Connection Attribute and Not-Connected Value settings match your charger's actual attributes (check Developer Tools → States). - Confirm
ev_low_carbon_nowison. If not, check the Z-score value and your chosen sensitivity mode. - Make sure the
ev_charge_modeselect is set toauto.
The Z-score shows as unavailable.
- The integration needs at least 2 readings to compute a Z-score. After a fresh install, wait a few minutes for the first backfill to complete.
- Check the HA logs (filter by
carbon_aware_ev_charging) for warmup messages.
I want to charge right now regardless of carbon.
- Set the
ev_charge_modeentity toforce_on.
A HA Repair issue appeared about a sensor being unavailable.
- The CO₂ or fossil fuel sensor has been returning no data for more than 30 minutes. Check the Electricity Maps integration or whichever integration provides your carbon data. The repair issue clears automatically once the sensor recovers.
The fossil fuel sensor is from a different source / has different units.
- Any HA sensor that exposes a 0–100 numeric percentage will work. Select it in Step 1 of the configuration.
Bug reports and pull requests are welcome at github.com/andrewreaganm/carbon-aware-ev-charging.
If you have tested this integration with a charger other than Emporia and it works, please open an issue to let us know so we can update the compatibility list.
This project is licensed under the GNU General Public License v3.0.
Logo provided by flaticon.com