Comprehensive reference for all entities exposed by the HSEM integration, including attributes, states, and dashboard examples.
HSEM exposes these entity types:
| Type | Count | Description |
|---|---|---|
| Sensor | ~40 | Read-only state, plan, diagnostic, financial, and EV entities |
| Select | 2 | Force working mode override and Solcast likelihood selector |
| Switch | ~12 | Toggle entities for EV settings, features, and ML options |
| Time | 2 | EV charge deadlines |
| Number | 4 | Charge/discharge efficiency and EV target SoC controls |
All attributes below are always available on the live state
(state_attr(), dashboards, automations). To keep the Home Assistant
database small (issue #1099), HSEM records only small scalar attributes.
Lists, dicts, per-slot timestamps and restore payloads are listed in each
sensor's _unrecorded_attributes and never reach the recorder:
| Sensor | Not recorded |
|---|---|
sensor.hsem_workingmode_sensor |
all attributes |
sensor.hsem_plan_explanation_sensor |
rejected_plans, constraints, current_slot_start, current_slot_end |
sensor.hsem_ev_*optimal_charging_plan |
charging_slots, planned_load_by_slot, data_quality |
sensor.hsem_ev_*soc_economics |
points, configured_deadlines, baseline_plan_cost |
sensor.hsem_ev_*charger_current_limit |
schedule |
sensor.hsem_solar_confidence_sensor |
hour_factors, processed_through, _solar_corrector_data |
sensor.hsem_forecast_accuracy_sensor |
_forecast_tracker_data |
sensor.hsem_prediction_accuracy_sensor |
action_mix |
sensor.hsem_savings_tracker_sensor |
daily |
sensor.daily_plan_vs_actual |
today, yesterday, history |
| Export income / import cost / net grid balance | today, last_7_days, last_30_days, this_month, this_year, daily |
sensor.hsem_performance_* |
yesterday, last_7_days, last_30_days, daily, notes |
sensor.hsem_applier_status_sensor |
last_apply_details, failed_entities, unverified_entities |
sensor.hsem_ocpp_*charger_status |
all attributes |
sensor.hsem_ocpp_*charger_sessions |
sessions |
sensor.hsem_next_update_sensor |
last_updated (recorded as the state of sensor.hsem_last_updated_sensor) |
sensor.hsem_house_consumption_energy_avg_* |
all except the state (measurements, last_updated, …) |
sensor.hsem_house_consumption_power_* |
all except the state |
State values are always recorded, so history graphs and long-term
statistics of the sensor states are unaffected. Restart restore reads
RestoreEntity storage (.storage/core.restore_state), which keeps every
attribute regardless of this list.
last_updated attributes. On the per-hour consumption sensors,
last_updated is the time the published value last changed. On the
working mode sensor and sensor.hsem_next_update_sensor it is the
coordinator heartbeat (time of the last completed update cycle), the same
value as the sensor.hsem_last_updated_sensor state. For "when did this
value last change" on any entity, use Home Assistant's built-in
last_changed ({{ states.sensor.x.last_changed }}).
Entities are split across 7 Home Assistant devices instead of one, so each subsystem gets its own device page and can be scoped independently in dashboards, automations, and Areas.
| Device | Identifier suffix | Entities |
|---|---|---|
| Controller | (none — legacy) | Working mode, degraded mode, read-only, hardware-writes, missing-entities, force-mode, last/next-updated, update-interval, applier-status, plan-explanation, daily-plan-vs-actual, recommendation-interval, force-working-mode selector, extended-attributes/verbose-logging/ML switches |
| Battery & Energy | _battery_energy |
Battery SoC, effective discharge floor, net consumption, PV curtailment, charge/discharge efficiency numbers, dynamic discharge floor switch |
| Hourly Consumption Profile | _hourly_consumption |
All 168 per-hour-block entities (HSEMHouseConsumptionPowerSensor + integral + utility-meter + 1/3/7/14-day averages × 24 hour blocks) |
| Financial | _financial |
Export income, import cost, net grid balance, savings, performance (savings capture, actual benefit, missed opportunity, battery and solar utilisation, EV charging efficiency) |
| Forecast | _forecast |
Forecast accuracy, prediction accuracy, solar confidence, Solcast likelihood selector |
| EV Primary | _ev_primary |
EV charging/plan sensors, calculated power, current limit, target SoC number, deadline time, smart-charging/force-charge-now/force-discharge/auto-full switches, OCPP charger sensors for charger_index=1 |
| EV Secondary | _ev_secondary |
The same set as EV Primary for charger_index=2 / the second EV (only present when the second EV is configured) |
The Controller device keeps the original (DOMAIN, entry_id) identifier that
predates the split — no migration is needed for its entities. Every other
device is identified as (DOMAIN, f"{entry_id}_<suffix>").
Naming: EV Secondary and OCPP entity names no longer carry a redundant
"Second"/"2" marker (e.g. "Charger Status", not "OCPP Charger Second Status") — the device name (e.g. "HSEM EV Secondary") already disambiguates
via _attr_has_entity_name = True. unique_id and entity_id are
unchanged.
Migration: on first startup after upgrading, a one-time migration
(custom_components/hsem/device_migration.py) reassigns device_id for
every pre-existing entity to its new device, gated by a migration-version
flag stored on the config entry so it runs exactly once. unique_id is
never touched. This is a breaking change for dashboards/automations that
reference the old single device_id.
The primary HSEM sensor. Exposes the active battery recommendation and carries all planner output as attributes.
Entity: sensor.hsem_workingmode_sensor
| State | Meaning |
|---|---|
batteries_charge_grid |
Battery charging from grid (forced by schedule or price) |
batteries_charge_solar |
Battery charging from PV surplus |
batteries_discharge_mode |
Battery discharging to cover house load |
batteries_discharge_window_mode |
Inside a seasonal discharge window, but the plan dispatches no discharge; self-consumption still allowed |
force_batteries_discharge |
Forced discharge to grid (excess export) |
force_export |
Negative import price — all energy exported |
ev_smart_charging |
EV charging load allocated |
batteries_wait_mode |
Battery idle; may allow self-consumption above the planner reserve depending on Wait mode behaviour |
time_passed |
Slot is in the past |
missing_input_entities |
Required HA entities unavailable |
| Attribute | Type | Description |
|---|---|---|
batteries_current_capacity |
float (kWh) | Current usable battery capacity above discharge floor |
batteries_usable_capacity |
float (kWh) | Usable battery capacity (rated − floor) |
batteries_recommended_min_price_threshold |
float | Discharge threshold from calculate_recommended_threshold() |
batteries_capacity_loss_pct |
float | Configured capacity loss percentage |
import_electricity_price_state |
float | Live import spot price (currency/kWh) |
export_electricity_price_state |
float | Live export spot price (currency/kWh) |
export_electricity_min_price |
float | Minimum export price for export gating |
electricity_price_update_interval |
int | Price refresh interval (minutes) |
house_consumption_power_state |
float (W) | Instantaneous house load |
house_power_includes_ev_charger_power |
bool | Whether EV load is baked into house power |
net_consumption |
float (W) | Net load (house − solar) |
net_consumption_with_ev |
float (W) | Net load including EV |
solar_production_power_state |
float (W) | Instantaneous solar production |
months_winter / months_summer |
list[int] | Configured winter/summer month ranges |
batteries_enable_excess_export |
bool | Excess export gating enabled |
batteries_excess_export_discharge_buffer |
float | Discharge buffer for excess export |
batteries_forecast_reserve_pct |
float | Extra SoC points above the hardware floor reserved immediately after intentional battery export (issue #807, 0 = disabled) |
phase_aware_charging_enabled |
bool | Live phase-aware grid-charge safety limiter enabled (issue #831, default False) |
grid_phase_readings |
tuple[[float, str] | None, …] | Live per-phase grid readings (a, b, c) as [value, unit], unit W (signed) or A (issue #1119); None per phase when unavailable or in an unsupported unit |
grid_phase_voltage_v |
tuple[float | None, float | None, float | None] | Live per-phase voltage (a, b, c) in volts; None when unconfigured or unavailable (230 V is then assumed) |
huawei_batteries_grid_charge_max_power_w |
float | None | Live Huawei grid-charge maximum-power ceiling in Watts, or None if unconfigured/unavailable |
primary_grid_charge_owned |
bool | Whether HSEM currently believes it owns an armed Huawei grid-charge cap (issue #840). Drives the Error-mode emergency stop; never true for a charge armed outside of HSEM |
battery_target |
dict | None | House-battery target SoC diagnostics for the next occurrence (issue #1109): target_time, target_kwh, stage1_projected_kwh, projected_kwh, shortfall_kwh, penalty_per_kwh, stage2_ran, stage2_status, max_import_delta_kwh, and the preference cost (issue #1185): stage1_cost, stage2_cost, preference_cost, preference_cost_per_kwh, terminal_soc_value_delta (null unless the target changed the plan). None when the target is disabled |
batteries_wait_mode_behavior |
string | Wait mode behaviour: strict or self_consumption_with_reserve |
wait_mode_self_consumption |
dict | What the live batteries_wait_mode slot does with the battery (issue #1255): active (the slot runs as self-consumption, the house may use the battery), reserve_kwh (energy held back, above the hardware floor), surplus_kwh (energy above the reserve). active is false and both figures are null when the behaviour does not apply: another state, strict, an EV charging or planned, a planned PV export, or no reserve. The planned SoC of a wait slot never includes this discharge |
house_consumption_energy_weight_1d |
float | 1-day consumption prediction weight |
house_consumption_energy_weight_3d |
float | 3-day weight |
house_consumption_energy_weight_7d |
float | 7-day weight |
house_consumption_energy_weight_14d |
float | 14-day weight |
last_updated |
string (ISO-8601) | Last coordinator cycle timestamp |
status |
string | ok, read_only, wait, or error |
degraded_mode |
string | ok, degraded, or error |
hardware_writes_blocked |
bool | Safety gate preventing hardware writes |
apply_status |
string | Last apply result: ok, unverified, failed, skipped |
apply_failed_entities |
list[string] | Entities that failed the last hardware write |
data_quality |
dict | Structured price/PV and load-forecast completeness report |
force_working_mode_state |
string | Active override mode or auto |
| Attribute | Type | Description |
|---|---|---|
hourly_recommendation |
dict | null | The recommendation slot active right now |
hourly_recommendations |
list[dict] | Full list of planner slots for the horizon |
Each entry in the hourly_recommendations list is a dictionary with these keys:
| Key | Type | Description |
|---|---|---|
start |
string (ISO-8601) | Slot start timestamp |
end |
string (ISO-8601) | Slot end timestamp |
recommendation |
string | null | Working-mode value (see state table above) |
import_price |
float | Spot import price (local currency/kWh) |
export_price |
float | Spot export price (local currency/kWh) |
avg_house_consumption_kwh |
float | Weighted spike-aware consumption estimate (kWh) |
avg_house_consumption_1d_kwh |
float | 1-day window contribution (kWh) |
avg_house_consumption_3d_kwh |
float | 3-day window contribution (kWh) |
avg_house_consumption_7d_kwh |
float | 7-day window contribution (kWh) |
avg_house_consumption_14d_kwh |
float | 14-day window contribution (kWh) |
solcast_pv_estimate_kwh |
float | Forecast PV production for the slot (kWh) |
estimated_net_consumption_kwh |
float | avg_consumption + ev_planned_load − pv_estimate (kWh) |
ev_planned_load_kwh |
float | Extra EV AC load added to net consumption (kWh, ≥ 0) |
ev_accounted_load_kwh |
float | EV AC load already in house consumption (kWh, ≥ 0) |
ev_total_planned_load_kwh |
float | Total EV AC load (planned + accounted, kWh, ≥ 0) |
ev_charger_calculated_power |
float | Primary EV charger target AC power (W) |
ev_second_charger_calculated_power |
float | Second EV charger target AC power (W) |
estimated_cost_currency |
float | Estimated grid cost for the slot (local currency) |
batteries_charged_kwh |
float | Energy scheduled to charge into battery (kWh) |
batteries_discharged_kwh |
float | Energy drawn from battery by SoC simulation (kWh) |
estimated_battery_capacity_kwh |
float | kWh above the hardware discharge floor at slot end |
estimated_battery_soc_pct |
float | Simulated absolute SoC at slot end (0–100 %) |
grid_import_kwh |
float | Energy imported from grid (kWh) |
grid_export_kwh |
float | Energy exported to grid (kWh) |
is_ev_surplus_only_slot |
bool | Slot restricted to EV surplus-only charging |
When the switch.hsem_extended_attributes switch is on, additional entity-ID
attributes are exposed. These reference the raw HA entity IDs for troubleshooting:
| Attribute | Description |
|---|---|
import_electricity_price_sensor_entity |
Import price sensor entity ID |
export_electricity_price_sensor_entity |
Export price sensor entity ID |
ev_charger_power_entity |
Primary EV charger power entity ID |
ev_charger_status_entity |
Primary EV charger status entity ID |
ev_soc_entity |
Primary EV SoC entity ID |
ev_connected_entity |
Primary EV connected entity ID |
ev_second_charger_power_entity |
Second EV charger power entity ID |
ev_second_charger_status_entity |
Second EV charger status entity ID |
ev_second_soc_entity |
Second EV SoC entity ID |
ev_second_connected_entity |
Second EV connected entity ID |
force_working_mode_entity |
Force working mode entity ID |
house_consumption_power_entity |
House consumption power entity ID |
solar_production_power_entity |
Solar production power entity ID |
solcast_pv_forecast_forecast_today_entity |
Solcast today forecast entity ID |
solcast_pv_forecast_forecast_tomorrow_entity |
Solcast tomorrow forecast entity ID |
(plus all huawei_solar_* entity IDs) |
Huawei battery and inverter entity IDs |
recommendation_interval_minutes |
Slot width in minutes |
recommendation_interval_length |
Number of slots in the horizon |
unique_id |
Integration unique ID |
update_interval |
Polling interval in minutes |
read_only |
Whether read-only mode is active |
| Attribute | Type | Description |
|---|---|---|
ev_charger_power_state |
float (W) | Primary EV charger instantaneous power |
ev_charger_status_state |
bool | Primary EV currently charging |
ev_soc_state |
float (%) | Primary EV current SoC |
ev_soc_target_state |
float (%) | Primary EV target SoC |
ev_connected_state |
bool | Primary EV plugged in |
ev_allow_charge_past_target_soc |
bool | Allow charging past target |
ev_past_target_confidence_factor |
float | Confidence factor (0.0–1.0) applied to the avoided-future-import valuation of past-target charging |
ev_charger_max_discharge_power_state |
float (W) | Battery discharge ceiling while this EV charges (once permission below is granted) |
ev_charger_force_max_discharge_power |
bool | Permission for the Huawei battery to discharge while this EV charges; without it, discharge is 0 W whenever the EV is active |
ev_second_enabled |
bool | Second EV integration enabled |
ev_second_charger_power_state |
float (W) | Second EV charger power |
ev_second_charger_status_state |
bool | Second EV charging |
ev_second_soc_state |
float (%) | Second EV SoC |
ev_second_soc_target_state |
float (%) | Second EV target SoC |
ev_second_connected_state |
bool | Second EV plugged in |
ev_second_allow_charge_past_target_soc |
bool | Second EV past-target flag |
ev_second_past_target_confidence_factor |
float | Second EV past-target confidence factor (0.0–1.0) |
ev_second_charger_max_discharge_power_state |
float (W) | Second EV: same discharge ceiling semantics as the primary EV |
ev_second_charger_force_max_discharge_power |
bool | Second EV: same discharge permission semantics as the primary EV |
| Attribute | Type | Description |
|---|---|---|
huawei_solar_batteries_charging_cutoff_capacity_state |
float (%) | Inverter charging cutoff SoC |
huawei_solar_batteries_grid_charge_cutoff_soc_state |
float (%) | Grid charge cutoff SoC |
huawei_solar_batteries_maximum_charging_power_state |
float (W) | Maximum charge power |
huawei_solar_batteries_maximum_discharging_power_state |
float (W) | Maximum discharge power |
huawei_solar_batteries_rated_capacity_max_state |
float (Wh) | Rated battery capacity |
huawei_solar_batteries_rated_capacity_min_state |
float (kWh) | Discharge floor capacity |
huawei_solar_batteries_state_of_capacity_state |
float (%) | Battery SoC |
huawei_solar_batteries_tou_charging_and_discharging_periods_periods |
list | Parsed TOU periods |
huawei_solar_batteries_tou_charging_and_discharging_periods_state |
string | Raw TOU entity state |
huawei_solar_batteries_working_mode_state |
string | Inverter working mode |
huawei_solar_inverter_active_power_control_state_state |
string | APC mode |
huawei_solar_batteries_excess_pv_energy_use_in_tou_state |
string | Excess PV in TOU setting |
Displays the planner's strategy rationale and per-candidate cost breakdown.
Entity: sensor.hsem_plan_explanation_sensor
| Key attribute | Description |
|---|---|
| State | Winning candidate name: "milp", "passive", "no_action" |
selected_strategy |
Human-readable description (e.g. "charge_grid_discharge_peak") |
winner_name |
Winning candidate name (same as state) |
summary |
One-sentence human-readable reason |
score |
Estimated savings vs doing nothing (currency) |
estimated_total_cost |
Net grid cost for the horizon |
price_spread |
Max minus min import price (arbitrage potential) |
peak_import_price / off_peak_import_price |
Price extremes |
forecast_pv_kwh |
Total PV forecast for the horizon |
forecast_net_consumption_kwh |
Total load minus PV |
battery_soc_pct / battery_soc_at_end_pct |
Starting and ending SoC |
constraints |
Active flags (winter_month, excess_export_enabled, etc.) |
rejected_plans |
Alternatives with name, reason, and full cost breakdown |
hysteresis_active |
Whether plan-level hysteresis was applied |
hysteresis_reason |
Explanation of hysteresis decision |
solver_status |
How the MILP solve ended (issue #1309): optimal, time_limit (the solver stopped at its time limit and its best plan so far is used; selected_strategy is then solver_time_limit), or failed (no MILP plan, a fallback is used; selected_strategy is solver_failed). null without scipy |
solver_time_s |
Wall time of that solve in seconds |
solver_gap_pct |
Relative MIP gap HiGHS reported, in percent (0 when proven optimal) |
solver_time_limit_s |
The solver time limit (hsem_planner_solver_time_limit_s) |
ml_forecast_wind_configured |
Whether wind chill is enabled and a weather entity is configured |
ml_forecast_wind_slots_used |
Future slots using per-slot forecast wind speed |
ml_forecast_wind_fallback_slots |
Future slots that fell back to the nearest measured wind reading |
Cumulative monetary sensors that track grid import cost and export revenue.
All three use total because signed prices may make their values decrease.
They count on every update cycle, whether or not HSEM is planning: a forced working mode, a missing entity or a load forecast that is not ready yet does not pause them. Only a stretch in which no cycle ran at all (Home Assistant down, or longer than two update intervals between cycles) is left out, because a meter delta that old cannot be priced.
Entities:
sensor.hsem_export_income— Cumulative export revenuesensor.hsem_import_cost— Cumulative import costsensor.hsem_net_grid_balance— Export income minus import cost
Attributes (all three): today, last_7_days, last_30_days,
this_month, this_year (each {import_cost, export_income, net_balance})
and daily — the newest 90 daily records, oldest first. The tracker keeps
366 completed days in hsem_financial_history.json so the this_year
rollup is always complete; older days are pruned at midnight and on load.
| Property | Value |
|---|---|
| Type | sensor |
| State class | total |
| State | Cumulative export revenue (local currency) |
| Device class | monetary |
| Property | Value |
|---|---|
| Type | sensor |
| State class | total |
| State | Cumulative import cost (local currency) |
| Device class | monetary |
| Property | Value |
|---|---|
| Type | sensor |
| State class | total |
| State | Net grid balance (export_income − import_cost, local currency) |
| Device class | monetary |
Template example:
{{ states('sensor.hsem_net_grid_balance') | float | round(2) }}Tracks prediction accuracy across multiple horizons — solar, load, and battery SoC — up to 30 days.
Entity: sensor.hsem_prediction_accuracy_sensor
| State | Meaning |
|---|---|
soc_mae_7d |
7-day battery SoC MAE (percentage points) |
| Attribute | Unit | Description |
|---|---|---|
soc_mae_7d |
pp | 7-day SoC Mean Absolute Error |
soc_mae_30d |
pp | 30-day SoC Mean Absolute Error |
solar_mape |
% | Solar forecast MAPE |
load_mae_kwh |
kWh | Load Mean Absolute Error |
action_mix |
dict | Distribution of planner actions over the window |
Per-hour PV forecast accuracy factors and confidence percentile.
Entity: sensor.hsem_solar_confidence_sensor
| Attribute | Description |
|---|---|
| State | Mean accuracy factor across the horizon (ratio) |
factors |
dict — per-hour correction factors for each slot |
confidence_pct |
float — confidence percentile (e.g. 50 = median) |
Template example:
{{ states('sensor.hsem_solar_confidence_sensor') | float | round(3) }}
Confidence: {{ state_attr('sensor.hsem_solar_confidence_sensor', 'confidence_pct') }}%Diagnostic sensors displaying the EV charging plan details.
Entities:
sensor.hsem_ev_optimal_charging_plan— Primary EV, created only whenhsem_ev_planned_load_enabledis setsensor.hsem_ev_second_optimal_charging_plan— Second EV, created only whenhsem_ev_second_planned_load_enabledis set (issue #859)
| State | Meaning |
|---|---|
not_connected |
EV is not plugged in |
smart_charging_disabled |
Smart charging turned off |
fully_charged |
Already at or above target SoC |
charging |
EV scheduled to charge in current slot |
waiting |
Connected but no active charging slot |
unavailable |
Not configured or capacity/power is zero |
Key attributes:
| Attribute | Description |
|---|---|
battery_capacity_kwh |
EV battery nameplate capacity |
charge_power_kw |
Charger AC output power |
current_soc / target_soc |
EV SoC values |
ev_connected |
Whether vehicle is plugged in |
total_kwh_needed |
Energy needed to reach target |
deadline |
ISO-8601 charging deadline |
charging_slots |
List of allocated charging slots with details |
planned_load_by_slot |
Dict of slot → kWh load |
data_quality |
Diagnostic warnings |
Diagnostic sensors that answer one question for each EV: what would it cost to charge from the current SoC to X % by time Y? They show a table of charging costs, in real money, for the target SoCs 50, 60, 70, 80 and 100 %, against a few deadlines:
- the configured times of day (default 08:00 and 17:00, up to 3 per EV,
set in Configure → EV planned load as
hsem_ev_planned_load_economics_deadlines/hsem_ev_second_planned_load_economics_deadlines), each at its next occurrence; and - the EV's own target deadline (
time.hsem_ev_deadline_time/time.hsem_ev_second_deadline_time), which is always added as an extra column while it is ahead of now (issue #1344). When it falls on a configured time of day, the two share one column.
No target is recommended: the sensor shows the numbers and you decide.
Entities:
sensor.hsem_ev_soc_economics— Primary EV, created only whenhsem_ev_planned_load_enabledis setsensor.hsem_ev_second_soc_economics— Second EV, created only whenhsem_ev_second_planned_load_enabledis set
Each row is one extra run of the planner (run_planner()) on a copy of the
input of the last plan. Only that EV's target SoC and deadline are changed;
prices, the PV forecast, the house battery, the other EV and every setting are
reused unchanged. Its result is the money cost of the whole plan over the
planning horizon (PlannerOutput.plan_cost.total_cost): grid import, minus
export income, plus house-battery wear, for the house and the car together.
That is plan_total_cost.
To isolate the car, one more run per EV sets only its target to its current
SoC, so the plan does no target-driven charging. That is the baseline
(baseline_plan_cost): what the house costs without charging the car any
further. Each row's total_cost is the difference (issue #1348):
total_cost = plan_total_cost − baseline_plan_cost
So total_cost is the charging cost from the current SoC to that row's
target by that deadline, including any change it causes in how the house
battery and export are used. The baseline does not depend on the deadline:
an EV already at its target gets no deadline in the plan, so one baseline
serves every column.
total_cost is not clamped at zero. It can come out slightly negative: the
planner minimises money plus the value of the energy left in the house
battery at the end, not money alone, so a plan with more car charging can
spend a little less money and leave the house battery a little emptier.
| State | Meaning |
|---|---|
ready |
Table computed. points may still be empty when no column or no target at or above the SoC remains |
not_connected |
EV is not plugged in. No table |
smart_charging_disabled |
Smart charging is off, or the EV planned load is disabled. No table |
unavailable |
No plan has run yet, the EV's SoC is unavailable, or capacity or charger power is 0 |
| Attribute | Description |
|---|---|
current_soc_pct |
EV SoC when the table was computed (null when unavailable — the table is then empty, never computed from a made-up 0 %) |
target_deadline |
The EV's target deadline (ISO 8601) when it is ahead of the computation time, else null. Shown in every state, also when the EV is not connected |
configured_deadlines |
The configured times of day the table was computed for, e.g. ["08:00", "17:00"]. Duplicates are removed and the list is sorted |
baseline_plan_cost |
Whole-plan cost with this EV's target at its current SoC (no target-driven charging), which every row's total_cost is measured against. null when no row was computed. Not recorded |
points |
Flat list of rows, one per (target SoC, deadline column). Columns are in time order, earliest deadline first; within a column, targets ascend. Not recorded (see below) |
Each entry in points has:
| Field | Description |
|---|---|
target_soc_pct |
Target SoC of this row |
deadline_label |
Local HH:MM of the column, e.g. "08:00". Unique per column |
deadline |
ISO 8601 deadline actually used. A configured time resolves to its next occurrence (today if still ahead, else tomorrow); the target-deadline column uses the exact deadline |
is_target_deadline |
true in the column of the EV's own target deadline |
total_cost |
Charging cost from the current SoC to this target by this deadline: plan_total_cost − baseline_plan_cost, in the price sensor's currency. 0.0 when the target equals the current SoC |
plan_total_cost |
Money cost of the whole plan (house and car, whole horizon) with this target and deadline. Equals baseline_plan_cost when the target equals the current SoC |
feasible |
Whether the charger can deliver the energy in time at its rated power: capacity × (target − SoC) ≤ charger kW × hours left × charger efficiency |
delta_from_previous |
total_cost minus that of the previous (lower) row in the same column: what the extra SoC costs. null for the first row of a column |
delta_per_10pct |
delta_from_previous scaled to 10 percentage points of SoC, to compare uneven steps (e.g. 80 → 100 % is a 20-point step). null for the first row |
Rows below the current SoC are left out. At 63 % the 50 and 60 % rows are
missing and every column starts at 70 %. The first row's total_cost is
already the full cost from the current SoC, so its delta_from_previous is
null. A target exactly equal to the SoC stays in with total_cost 0.0;
it reuses the baseline run instead of a run of its own.
feasible only checks power and time. It ignores price, the grid and
phase fuse limits, the charger's minimum power, the deadline safety margin and
the other EV sharing the connection, so a true row can still fall short in
the real plan. When a row is false, the planner charges as much as it can
before the deadline and stops: its total_cost is the cost of that shortfall
plan, not of reaching the target. That is why infeasible rows often show a
delta of 0 — higher targets cannot be reached either, so they cost the same.
total_cost before issue #1348 was the whole-plan cost. Automations or
templates that read total_cost as the plan cost should read
plan_total_cost instead.
Worked example. An EV with a 77 kWh battery and an 11 kW charger at 35 % SoC, computed at 14:00, target deadline 07:00, default configured deadlines, a house using 3 kWh an hour, prices 0.05 at night, 0.10 by day and 0.30 from 16:00 to 21:00. The baseline (no further charging) costs 3.44:
Column (deadline_label) |
Target | total_cost |
plan_total_cost |
delta_from_previous |
delta_per_10pct |
feasible |
|---|---|---|---|---|---|---|
17:00 (today) |
50 % | 1.17 | 4.61 | ✓ | ||
| 60 % | 1.93 | 5.37 | +0.76 | 0.76 | ✓ | |
| 70 % | 3.73 | 7.17 | +1.79 | 1.79 | ✓ | |
| 80 % | 5.52 | 8.96 | +1.79 | 1.79 | ⚠ | |
| 100 % | 5.52 | 8.96 | +0.00 | 0.00 | ⚠ | |
07:00 (is_target_deadline: true) |
50 % | 1.17 | 4.61 | ✓ | ||
| 60 % | 1.93 | 5.37 | +0.76 | 0.76 | ✓ | |
| 70 % | 2.71 | 6.16 | +0.78 | 0.78 | ✓ | |
| 80 % | 3.47 | 6.91 | +0.76 | 0.76 | ✓ | |
| 100 % | 5.01 | 8.46 | +1.54 | 0.77 | ✓ | |
08:00 (tomorrow) |
... |
Read it like this:
- Charging from 35 to 80 % by 07:00 costs 3.47 (34.7 kWh, about 0.10 per kWh). The plan as a whole costs 6.91, of which 3.44 is the house.
- By 07:00, each extra 10 % (7.7 kWh) costs between 0.76 and 0.78.
- By 17:00, 70 % costs 1.79 more than 60 %: with only three hours left, the charging reaches into the 0.30 peak that starts at 16:00. Reaching 70 % by 07:00 instead costs 1.01 less (2.71 against 3.73).
- 80 % and 100 % cannot be reached by 17:00 (34.7 kWh in 3 hours at 11 kW):
both rows are
falseand show the same cost, the cost of charging flat out. - Here the 08:00 column repeats the 07:00 one, because the extra hour adds no
cheaper price. The
delta_per_10pctof 100 % (0.77) is half itsdelta_from_previous(1.54), because 80 → 100 % is a 20-point step.
Each row needs one planner run for every target above the current SoC in every column, plus one baseline run per EV, so up to targets × columns + 1 runs per EV:
- default (08:00, 17:00 and the target deadline): up to 5 × 3 + 1 = 16 per EV;
- most (3 configured times plus the target deadline): up to 5 × 4 + 1 = 21 per EV.
Both EVs are computed in one background job with a 60 s limit. If that job is still running, or runs out of time, the previous table stays.
The table is recomputed on every fresh replan, the same events that update
sensor.hsem_ev_optimal_charging_plan (EV plug/unplug, charging start/stop,
target-SoC threshold crossings, slot boundaries, price changes). It is also
recomputed at once when its columns no longer match: a changed list of
configured times, or a target deadline that changed or just passed. Otherwise,
while the previous plan is being reused, it is recomputed at most every
30 minutes.
points and configured_deadlines are lists and baseline_plan_cost
changes on every recompute, so they are never written to the recorder
(issue #1099): read them from the live state. target_deadline,
current_soc_pct and the state are recorded.
points is flat and already in time order. To show one line per column, walk
the distinct deadline values in order, as the bundled dashboard's EV
Charging Economics card does (see dashboard-setup.md);
it marks the target-deadline column with ★. Jinja's groupby('deadline_label')
works too, but sorts the columns by label instead of by time.
Diagnostic sensors exposing the planner's calculated EV charger power for the current slot as their state, so it can be graphed and used in automations without parsing the working-mode sensor attributes.
Entities:
sensor.hsem_ev_charger_calculated_power— Primary EV, created only whenhsem_ev_planned_load_enabledis setsensor.hsem_ev_second_charger_calculated_power— Second EV, created only whenhsem_ev_second_planned_load_enabledis set (issue #859)
| State | Unit | Description |
|---|---|---|
| ≥ 0 | W | Target AC power for the charger in the active slot (0 when no charging is planned) |
Key attributes:
| Attribute | Description |
|---|---|
slot_start / slot_end |
ISO-8601 window of the active planning slot |
ev_total_planned_load_kwh |
Total EV AC load planned for the slot (kWh) |
These mirror ev_charger_calculated_power and
ev_second_charger_calculated_power from the hourly_recommendation attribute
of the working-mode sensor.
Diagnostic sensors publishing the planner's per-slot EV charging ceiling in whole amps, for an external current controller to consume. HSEM owns the economics (how many amps are worth drawing this slot); the external controller keeps final authority for fuse safety and may only ramp within the published ceiling. Conversion always rounds down and is phase-aware (see planner-spec.md Published charging ceiling and stranded-residue re-portioning).
Entities:
sensor.hsem_ev_charger_current_limit— Primary EV, created only whenhsem_ev_planned_load_enabledis setsensor.hsem_ev_second_charger_current_limit— Second EV, created only whenhsem_ev_second_planned_load_enabledis set (issue #859)
| State | Unit | Description |
|---|---|---|
| ≥ 0 | A | Ceiling for the active slot, floored to whole amps (0 when no charging is planned) |
Key attributes:
| Attribute | Description |
|---|---|
phase_topology |
The charger's configured phase topology used for the conversion |
schedule |
Up to 24 future slots, each {start, current_a, power_w} |
Startup and a failed coordinator cycle are fail-closed: the entity is
unavailable and its native value is 0. A previously restored positive
state is never accepted as a live actuator command (issue #789) — only a
successful, live coordinator recommendation can publish a positive ceiling.
Diagnostic sensor tracking daily cumulative plan-vs-actual energy deviations.
Entity: sensor.daily_plan_vs_actual
Tracks planned kWh vs actual kWh for import, export, PV, consumption, and battery throughput on a per-calendar-day basis using cumulative energy meter readings.
Boolean sensor indicating whether any EV is actively drawing power.
Entity: sensor.hsem_ev_charging_sensor — created only when hsem_ev_planned_load_enabled is set (issue #859)
| State | Meaning |
|---|---|
on |
At least one EV is charging |
off |
No EV is charging |
Key attributes:
| Attribute | Meaning |
|---|---|
ev_soc_pct / ev_second_soc_pct |
Vehicle integration's reported SoC |
ev_effective_soc_pct / ev_second_effective_soc_pct |
Reported SoC plus bounded delivered-energy credit when session identity is valid, else None |
ev_delivered_energy_credit_kwh / ev_second_delivered_energy_credit_kwh |
Battery-side measured energy not yet reflected by vehicle telemetry |
ev_power_w / ev_second_power_w |
Live measured charger power |
Delivered-energy credit is in memory only (issue #789). It clears on
disconnect, restart, unsafe session/SoC identity changes, and battery
capacity changes. Invalid power readings and excessive sample gaps add no
energy and break the integration interval without erasing credit already
validated in the same identified session. See
planner-spec.md Session EV invariant and
utils/ev_delivered_energy.py.
Snapshot of the battery state of charge with optional learned capacity tracking.
Entity: sensor.hsem_battery_soc_sensor
| State | Unit | Description |
|---|---|---|
| 0–100 | % | Battery SoC percentage |
Key attributes:
| Attribute | Unit | Description |
|---|---|---|
learned_capacity_kwh |
kWh | Learned usable battery capacity from charge/discharge cycles |
capacity_samples |
int | Number of charge/discharge samples contributing to the learned capacity |
The house's own power draw derived from the Huawei inverters and power meter (issue #1310), for installations without an EMMA or SDongle load sensor. Created only when Inverter Active Power and Power Meter Active Power are set in the Power step.
Entity: sensor.hsem_house_load_power
| State | Unit | Description |
|---|---|---|
| a float | W | Σ inverter active power − power meter active power (export positive) |
It is unavailable, never 0, while any input is unavailable or
non-numeric. Inputs in kW are converted to W. HSEM reads it as the house
consumption power when hsem_house_consumption_power is empty or points to an
entity that does not exist. See
home.md.
| Attribute | Description |
|---|---|
inverter_active_power_entities |
The inverters' active power entities |
power_meter_active_power_entity |
The power meter's active power entity |
Controls and reports the effective discharge floor SoC: the reserve the battery needs now to carry the house to the next refill, when the dynamic floor feature is enabled.
The state is the configured minimum SoC plus the reserve: reserve_kwh × safety_margin ÷ rated capacity × 100, capped at the configured maximum SoC (issue #1221). A reserve_kwh of 1.7 on a 10 kWh battery with a 5 % minimum and a margin of 1.15 reads 24.6 %, and the battery then holds 1.96 kWh above its minimum. Before that fix the state was the reserve's share of the usable capacity read as an absolute SoC (20.6 % in the same case), which held less than the reserve and could exceed 100 %.
The sensor reports the reserve at the start of the current slot. The planner does not hold that value for the whole horizon: the reserve declines slot by slot and is gone after the refill (issue #1188). Within the current slot the plan may take the battery down to the next slot's reserve, so the live SoC can read slightly below this sensor until the next replan. When the live SoC is already below the reserve, the battery serves the most expensive hours until the refill and waits in the cheapest ones (issue #1222); estimated_battery_soc_pct in the plan always starts at the inverter's reading (issue #1094).
The refill scan reads planned grid charges from a floor-free reference solve in the same replan (issue #1140). When that plan charges from the grid before the next solar surplus, the refill_type attribute is grid_charge and reserve_kwh is the house load until the charge, whatever the size of the charge (issues #1214, #1220); the load behind the charge is not reserved. A charge that only buys back the excess-export buffer after the plan sold the battery is not a refill (issue #1239): the night behind it is still reserved. When the plan charges in the current slot, reserve_kwh is 0 and the floor equals the configured minimum SoC. A planned charge counts from the first slot priced like the one it is planned in (within one battery cycle cost), so next_refill_slot can be earlier than the slot the plan charges in (issue #1198). When it does not, but a slot before the next solar surplus is priced within one battery cycle cost of the cheapest import price in the next 48 h and the battery could charge enough there, the floor is released the same way with refill_type: grid_available (issue #1156). When it does not either, but a slot before the next solar surplus is priced within one battery cycle cost of the cheapest price between it and that surplus, with dearer slots behind it, the bridge ends at the first such slot with refill_type: cheap_window (issue #1247): the plan can buy there for the dearer slots whether or not it does, so a 0.15 night before a 0.12 day reserves the evening before it and nothing behind it, and inside the window the floor is the configured minimum. Otherwise the floor bridges to the next solar surplus (refill_type: solar_surplus). Between replans the sensor keeps the floor the current plan was solved with.
While the floor reserves energy, the constraints attribute of sensor.hsem_plan_explanation_sensor lists dynamic_discharge_floor, and battery_below_dynamic_floor when the live SoC is at or below it (issue #1227). With battery_below_dynamic_floor listed, batteries_wait_mode with grid import in the cheapest hours before the refill is the floor keeping the battery for the more expensive ones.
Entities:
sensor.hsem_effective_discharge_floor_sensor— Current effective floor SoC (%)switch.hsem_dynamic_discharge_floor— Enable/disable the dynamic floor feature
| Property | Value |
|---|---|
| Type | sensor |
| State | Current effective discharge floor SoC percentage |
| Unit | % |
| Property | Value |
|---|---|
| Type | switch |
| State | on (dynamic floor active) or off (static floor) |
Template example:
{% if is_state('switch.hsem_dynamic_discharge_floor', 'on') %}
Dynamic floor: {{ states('sensor.hsem_effective_discharge_floor_sensor') }}%
{% endif %}Sensors providing live status and diagnostics for an OCPP-compliant EV charger connected via the integrated OCPP server. HSEM runs one OCPP server per EV (primary EV on port 9000 by default, second EV on port 9001). The primary sensors below are only created when hsem_ocpp_enabled is set; the _second variants exist only when the second EV is configured and the second OCPP server is enabled (issue #859).
Configuration keys (set in config flow):
| Key | Description |
|---|---|
hsem_ocpp_enabled |
Enable the OCPP server |
hsem_ocpp_port |
TCP port for the primary EV's OCPP WebSocket connections |
hsem_ocpp_cpid |
OCPP charge point identifier |
hsem_ocpp_start_window_s |
Seconds before charge deadline to start charging |
hsem_ocpp_stop_window_s |
Seconds after charge deadline to stop charging |
hsem_ocpp_second_enabled |
Enable the dedicated second-EV OCPP server (requires second EV) |
hsem_ocpp_second_port |
TCP port for the second EV's OCPP WebSocket connections |
hsem_ocpp_second_cpid |
Second EV charger's OCPP charge point identifier |
| Property | Value |
|---|---|
| Type | sensor |
| State | not_configured (this EV's OCPP server isn't enabled), disconnected (enabled, no charger connected), or the live connection/charging state (Available, Preparing, Charging, Finishing) |
| Attributes | listening (bool — server socket bound), port, requested_current_a (whole amps in the last SetChargingProfile sent to the charger, None until the first profile has been sent — issue #886), anti_flap_state ("idle"/"starting"/"charging"/"stopping" — HSEM's own anti-flap state machine, issue #892), stalled (bool — True when the active session has been stuck reporting "SuspendedEVSE"/"Faulted"/"Unavailable" for over 5 minutes despite an open transaction; "SuspendedEV", an EV-decided pause, is never flagged — issue #894), url (best-effort ws://<host>:<port>/<cpid>, or wss:// with OCPP TLS on (issue #1341), for the EVSE's OCPP config, including the configured charge-point ID, when HA can resolve a reachable host) — present only when this EV's OCPP server is enabled; plus one entry per connected charger CPID with status, power_w, transaction_id, connected_at, status_changed_at (issue #894) |
| Property | Value |
|---|---|
| Type | sensor |
| State | Live charging power (kW) |
| Device class | power |
| Unit | kW |
Reports 0 whenever the connector's status is anything other than
"Charging" (issue #969) — previously it kept showing the last
MeterValues reading verbatim, so e.g. a connector that had just
transitioned to SuspendedEVSE could still display several kW with no
fresh meter data to explain why.
| Property | Value |
|---|---|
| Type | sensor |
| State | Charger summary string |
| Attributes | vendor, model, firmware_version, serial_number |
| Property | Value |
|---|---|
| Type | sensor |
| State | Number of completed sessions |
| Attributes | sessions — list of completed session logs (start time, energy, duration) |
When the second EV is configured with its own OCPP server, four additional
diagnostic sensors are created with _second suffixed entity IDs:
sensor.hsem_ocpp_charger_status_sensor_secondsensor.hsem_ocpp_charger_power_sensor_secondsensor.hsem_ocpp_charger_info_sensor_secondsensor.hsem_ocpp_charger_sessions_sensor_second
These mirror the primary sensors but read from the second EV's dedicated OCPP server.
Template example:
{{ states('sensor.hsem_ocpp_charger_status_sensor') }}
{{ states('sensor.hsem_ocpp_charger_power_sensor') | float | round(2) }} kWTracks actual vs missed savings over a rolling 90-day window. Savings include export revenue, avoided grid-import cost from battery discharge, and the price difference when battery energy is charged below the mean positive import price for the current cycle's calendar day. Missing, non-finite, and non-positive planner prices are excluded from that daily mean.
The discharge part needs the battery's Charge/discharge power sensor (Huawei Solar step of the configuration). It is optional: without it, or while it is unavailable, savings from battery discharge are not counted and the figure covers export revenue and cheap charging only.
Entity: sensor.hsem_savings_tracker_sensor
| Property | Value |
|---|---|
| Type | sensor (diagnostic) |
| State | Today's actual savings (local currency): today_actual |
| Device class | monetary |
| State class | total |
Three figures are tracked, each for today, the last 7 and 30 days, and in total since the integration was installed:
- actual — savings that accrued while HSEM was in control (force mode
auto); - missed — savings that accrued while a working mode was forced: export revenue and discharge savings of that time are booked here instead of under actual. Charge savings need a plan and are not counted while a mode is forced;
- baseline — what a passive installation without a battery would have paid for the same house load and PV.
| Attribute | Description |
|---|---|
today_actual, today_missed, today_baseline |
Today's figures |
last_7_days_actual, last_7_days_missed |
Sums over the last 7 days, today included |
last_30_days_actual, last_30_days_missed |
Sums over the last 30 days, today included |
total_actual, total_missed, total_baseline |
Cumulative since the integration was installed |
daily |
Daily records, newest 90: date, actual_savings, missed_savings, baseline_cost |
max_history_days, history_total_days |
Days kept, and days currently held |
daily is not written to the recorder (see
Recorder footprint); the history is kept in
.storage/hsem_savings_history.json.
These are per-cycle estimates. For how the day compares with the best it could have been, see the Performance sensors.
Template example:
{% set s = 'sensor.hsem_savings_tracker_sensor' %}
Today: {{ state_attr(s, 'today_actual') }} (missed {{ state_attr(s, 'today_missed') }})
Last 30 days: {{ state_attr(s, 'last_30_days_actual') }}How good the last day's control turned out to be (issue #1182), where what was missed was lost: in battery dispatch or in the handling of PV surplus (issue #1270), and how well the timing of EV charging was chosen (issue #1271).
Entities (on the HSEM Financial device, all with the performance
prefix):
| Entity | State | Unit |
|---|---|---|
sensor.hsem_performance_savings_capture |
Share of the achievable savings that HSEM actually achieved | % |
sensor.hsem_performance_actual_benefit |
What HSEM's control was worth against plain self-consumption | local currency |
sensor.hsem_performance_missed_opportunity |
What perfect hindsight would have saved on top of that | local currency |
sensor.hsem_performance_battery_utilisation |
Share of the potential not lost through battery dispatch | % |
sensor.hsem_performance_solar_utilisation |
Share of the potential not lost through PV surplus handling | % |
sensor.hsem_performance_ev_charging_efficiency |
Share of what the best charging times save over plug-and-charge that was realized (only with EV planning enabled) | % |
All of them cover the trailing 24 hours, are updated once per completed slot,
and are diagnostic sensors with the measurement state class. They are rolling
values, not totals, so they carry no monetary device class (Home Assistant
allows that one only on totals); the amounts use Home Assistant's currency as
their unit.
The bundled dashboard has a Performance view that shows all of this, including the 7- and 30-day figures and a day-by-day breakdown: see Performance view.
Three ways the same 24 hours could have gone, over the same slots, the same realized prices and the same load:
| Run | What it is |
|---|---|
| realized | What was paid: grid import × import price − grid export × export price |
| baseline | Plain inverter self-consumption — the hardware with nobody controlling it |
| oracle | The cheapest the 24 hours could have been, with perfect foresight |
From them:
| Figure | Definition | Meaning |
|---|---|---|
| potential | baseline − oracle |
What the best possible control could have saved |
| missed opportunity | realized − oracle |
What was left on the table (never negative) |
| actual benefit | potential − missed |
What HSEM's control was worth against self-consumption |
| savings capture | benefit ÷ potential |
The share of the potential that was realized |
Worked example. Self-consumption would have cost 5.00, HSEM's day cost 3.40, and the best possible day costs 3.00, all three ending with the same energy in the battery. Potential 2.00, missed opportunity 0.40, actual benefit 1.60, savings capture 80 %.
Three rules keep the comparison fair:
- The battery must end as full as it really did. A run that ends with a fuller battery has paid for energy it has not used yet. Left alone, the oracle would win every day by emptying the battery, and every reserve would look like a mistake. So each oracle starts with the energy the battery really had and must end with at least the energy of the run it is compared with. Energy HSEM still holds at the end of the window is therefore not missed; holding it during the window while importing at a high price is.
- Your own settings are not a missed opportunity. The oracle respects the hard limits: usable capacity, charge and discharge power, efficiencies, the main fuse, the export limit, the export fee, and whether (and from which export price) you let the battery export at all. It does not get HSEM's own caution — dynamic floor, reserves, hysteresis, wear pricing, terminal value — because that is what is being measured.
- The EV is fixed load. The load is everything that is not the battery,
read off the meters as
grid import − grid export − battery charged + battery discharged. The EV is inside it in all three runs, so when the car was charged is not scored yet (that is stage 3).
The missed opportunity is split in two by one more hindsight run that sits between the realized day and the oracle:
| Run | What it may decide |
|---|---|
| realized | Nothing: what happened |
| PV-as-realized oracle | Grid charging and discharging, with perfect foresight; PV surplus is stored and exported as it really was |
| oracle | Everything |
Each run can do everything the one above it can, so their costs are ordered, and the two differences add up to the missed opportunity exactly:
| Figure | Definition | Meaning |
|---|---|---|
| battery loss | realized − PV-as-realized |
What better grid charging and better-timed discharging would have saved |
| solar loss | PV-as-realized − oracle |
What storing or exporting the PV surplus at better times would have saved |
| battery utilisation | 1 − battery loss ÷ potential |
Share of the potential not lost through battery dispatch |
| solar utilisation | 1 − solar loss ÷ potential |
Share of the potential not lost through PV surplus handling |
Both percentages use the same potential as the savings capture, so the shortfalls add up: a capture of 65 % with battery utilisation 72 % and solar utilisation 93 % reads "35 points were lost, 28 on battery dispatch and 7 on PV surplus".
A reserve held through an expensive evening is battery loss. Surplus sold cheaply at noon and bought back in the evening is solar loss. A slot's PV surplus is what its PV left over after the load; "as it really was" means the oracle stores exactly as much of it as was really stored (and may add grid charge on top where all of it was stored).
Storing surplus and using it later are two decisions, and the value of the pair is booked under solar. The split says where a loss became visible, not which setting caused it: surplus that could not be stored because the battery had been filled from the grid overnight shows up as solar loss.
On the real days committed in the repository (a 15 kWh installation) most of what was missed is battery dispatch, with solar utilisation around 90 % on the ordinary days:
| Day | Missed | Battery loss | Solar loss | Capture | Battery | Solar |
|---|---|---|---|---|---|---|
| 2026-09-15 | 3.26 | 2.57 | 0.70 | 65.0 % | 72.4 % | 92.5 % |
| 2026-09-26 | 19.51 | 13.03 | 6.48 | −26.7 % | 15.3 % | 57.9 % |
| 2026-09-29 | 2.29 | 1.77 | 0.53 | 54.5 % | 65.0 % | 89.5 % |
(from the backtest's battery energy meters; the sensors use SoC-derived flows and differ by a few points, see The same numbers as the backtest below).
sensor.hsem_performance_ev_charging_efficiency exists when EV planning is
enabled for at least one charger. It scores the timing of EV charging:
the energy the car got is taken as given, and only when it was delivered is
compared.
A session is, per charger, a run of consecutive slots in which the car was plugged in and HSEM was managing the charging: EV planning enabled, smart charging on, "force charge now" off. Inside a session the energy that was really delivered may be moved, but not across the charge deadline. Everything else the charger delivered stays fixed load, as in the figures above.
Three hindsight runs, all with the battery dispatched optimally, differ only in where the session energy sits:
| Run | EV energy |
|---|---|
| plug-and-charge | Every session delivered at full charger power from the moment the car was plugged in |
| oracle | Delivered when it really was (the oracle of the figures above) |
| joint oracle | Delivered at the cheapest times within each session |
| Figure | Definition | Meaning |
|---|---|---|
| EV missed opportunity | oracle − joint oracle |
What better timing would have saved (never negative) |
| EV potential | plug-and-charge − joint oracle |
What the best timing saves over none |
| EV charging efficiency | 1 − EV missed ÷ EV potential |
The share of that potential that was realized |
100 % is the best possible timing, 0 % is no better than plugging in and
charging, and the figure is negative when the timing was worse than that. It
is unknown when the EV potential is below 0.05 or no managed session
delivered energy.
The EV figure is on top of the missed opportunity, not part of it. The missed opportunity, battery and solar figures treat the EV as fixed load and do not change. The joint oracle can do everything the oracle can, so
realized − joint oracle = battery loss + solar loss + EV missed opportunity
exactly. The EV is scored against an optimally run battery, not against the battery as it ran: that is what makes the parts add up, and it keeps a badly run battery from making the EV timing look good or bad.
What it needs and what it leaves out:
- A charger power sensor and a connected sensor for the charger. Without a connected sensor there is no session to move energy within.
- The charging power is capped at the configured charger power, or at the highest power the session itself reached when none is configured.
- The oracle may charge at any power up to that cap. It does not know the charger's minimum current or its phase switching, so it is a little more optimistic than the hardware allows.
- The EV energy comes from the charger's power reading, sampled once per update cycle. At the default 5-minute cycle that is coarse.
- A session that reaches past the edge of the 24-hour window is scored for the part inside it.
- The definition has not yet been checked against a real installation with an EV; the repository's recorded days carry no EV data.
The oracle knows the day's PV, load and prices exactly. HSEM planned from
forecasts. 100 % is not reachable, and a capture of 60–80 % on a day with
an ordinary forecast error is a good result. What the figure is for is the
trend, and days that stand out. Telling forecast error from planner error is
what the offline backtest does (scripts/backtest_attribute.py, see
backtest-harness.md).
Which number to watch. On a day with flat prices there is almost nothing to
gain, and a share of almost nothing is noise: below a potential of 0.05 (in
your currency) the savings capture is unknown. Missed opportunity stays
meaningful on such days and is the more robust headline. A capture can be
negative — the day cost more than plain self-consumption would have — and
is reported as such, never clamped to 0.
sensor.hsem_savings_tracker_sensor answers a different question, and its
missed figures mean something else:
| Savings tracker | Performance sensors | |
|---|---|---|
| "Actual" | Sum of per-cycle estimates (export revenue, cheap charging…) | baseline − realized, with the battery's end energy equalised |
| "Missed" | Savings that accrued while force mode was not auto |
What perfect hindsight would have saved on top |
| Compared with | A live estimate of a house without a battery | The same slots run as self-consumption and as the best possible day |
All performance sensors publish the same attributes:
| Attribute | Description |
|---|---|
reason |
Why a value is unknown (see below); null when all three are known |
actual_benefit, missed_opportunity, savings_capture |
The three figures of the window (capture in percent) |
potential |
baseline − oracle |
battery_loss, solar_loss |
The two parts of the missed opportunity (they add up to it) |
battery_utilisation, solar_utilisation |
The same as shares of the potential, in percent |
breakdown_reason |
Why the battery and solar figures are unknown; null when they are known |
ev_missed_opportunity, ev_potential |
What better EV charging times would have saved, and what the best times save over plug-and-charge |
ev_charging_efficiency |
1 − ev_missed_opportunity ÷ ev_potential, in percent |
ev_shiftable_kwh |
EV energy delivered in managed sessions |
ev_reason |
Why the EV figures are unknown; null when they are known |
realized_cost, baseline_cost, oracle_cost |
The cost of each run |
window_start, window_end, slot_count |
The 24 physical hours that were scored |
coverage |
Share of the window that was observed (0–1) |
control_coverage |
Share of the observed time in which HSEM was in control (0–1) |
start_stored_kwh |
Energy in the battery at the start of the window |
realized_end_stored_kwh, baseline_end_stored_kwh |
Energy the real run and the self-consumption run end with |
residual_kwh |
Energy the meters cannot explain (see below) |
solver_status |
optimal, unsolved, unavailable or not_run |
notes |
Anything about the data that qualifies the score |
yesterday |
The same figures for the last completed local day |
last_7_days, last_30_days |
Benefit, missed opportunity, potential and capture over completed days |
daily |
The newest 30 completed days one by one, oldest first: date, reason, the three savings figures, potential, the battery and solar losses and utilisations, ev_missed_opportunity and ev_charging_efficiency. For the dashboard charts |
control_coverage tells a planner miss from a day HSEM was not driving:
it is below 1 when force mode was not auto, HSEM was read-only, or the cycle
was degraded. A poor score with a low control coverage is not the planner's.
Periods are ratios of sums. last_7_days and last_30_days add up the
costs of the completed local days and divide once; they are never a mean of
daily percentages, which would let a day with nothing to gain weigh as much as
a day with a lot. scored_days says how many of the period's days carry a
score; a day that could not be scored is left out, never counted as zero.
No number is published that cannot be stood behind. reason is one of:
reason |
Meaning |
|---|---|
not_evaluated |
Nothing has been scored yet |
insufficient_coverage |
Part of the window was not observed: a slot is missing or was observed for less than 90 % |
no_site_limits |
HSEM has not planned yet since it was installed, so the battery's limits are not known |
potential_below_threshold |
Almost nothing could have been saved: only the capture is unknown, the two amounts are valid |
stored_energy_outside_limits |
The battery's SoC ran more than 10 % of the usable capacity outside the configured SoC limits |
energy_balance_residual |
The meters contradict each other (see residual_kwh) |
scipy_unavailable |
The solver library is not installed |
oracle_unsolved |
The best possible day could not be found, or not proven best, within 10 seconds |
evaluation_failed |
The scoring raised an error (see the HSEM log) |
breakdown_reason takes the same values, and one more: breakdown_unsolved
when the score itself is valid but the extra hindsight run behind the split
could not be solved in time. The savings capture, actual benefit and missed
opportunity are then published as usual and only the two utilisation sensors
are unknown.
In the last_7_days and last_30_days attributes the battery and solar
figures are taken over the days that carry the split (breakdown_days), with
those days' own potential. Days scored before this split existed count towards
scored_days but not towards breakdown_days.
ev_reason takes the same values as reason, and two more: no_ev_session
when no managed session delivered energy that could have been moved, and
ev_unsolved when the two EV hindsight runs could not be solved in time. The
EV period figures are taken over the days that carry an EV score (ev_days).
A new installation shows insufficient_coverage for its first 24 hours. So
does a restart that kept Home Assistant down for more than about eight
minutes: the energy meters keep counting, so a short gap loses nothing, but a
long one cannot be placed in time.
- Grid import and export energy meters configured in HSEM (the same ones the financial sensors use). Without them there is no realized cost.
- Import and export price sensors with a current value.
- The battery's flows are taken from the change in its state of charge, through the configured rated capacity and efficiencies. No battery power sensor is needed.
- PV production (the PV energy meter, or the PV power sensor as a fallback) is
used for
residual_kwhand to let the oracle curtail PV when export prices are negative.
residual_kwh is a consistency check: a house cannot have a negative
load, so the running total of grid import − grid export − battery flows + PV must never fall. The largest fall is energy the meters cannot explain. It
stays near zero on a consistent installation and grows when a meter, a unit or
the configured battery capacity is wrong; above the largest of 1 kWh, 15 % of
the usable capacity and 10 % of the PV produced, the window is not scored.
The definitions and the code are shared with the offline backtest
(custom_components/hsem/planner/hindsight_scoring.py). A local day scored by
the sensors (yesterday) equals
python3 scripts/backtest_score.py <actuals> --battery-flows socon the same slots. Without --battery-flows soc the backtest reads the
battery's energy meters instead of its SoC, and the two differ by a few
points of capture on real days; see
backtest-harness.md.
The record is kept in .storage/hsem_performance_history.json: the slots of
the last 50 hours and the scores of the last 90 completed days. It survives a
restart.
Template example:
{% set s = 'sensor.hsem_performance_savings_capture' %}
Last 24 h: {{ states(s) }} % ({{ state_attr(s, 'missed_opportunity') }} missed)
Last 30 d: {{ state_attr(s, 'last_30_days').savings_capture }} %Detects when the inverter is actively curtailing PV production.
Entity: sensor.hsem_pv_curtailment_sensor
| Property | Value |
|---|---|
| Type | sensor |
| State | curtailed (PV being limited) or normal (no curtailment) |
| Entity category | diagnostic |
Template example:
{% if is_state('sensor.hsem_pv_curtailment_sensor', 'curtailed') %}
PV is being curtailed
{% endif %}| Entity | Display Name | Purpose | State / Value |
|---|---|---|---|
sensor.hsem_applier_status_sensor |
Inverter Apply Status | Hardware write success/failure | ok, unverified, failed, skipped |
sensor.hsem_battery_soc_sensor |
Battery State of Charge | Battery SoC snapshot | Percentage (0–100) |
sensor.daily_plan_vs_actual |
Daily Plan vs Actual | Daily energy plan-vs-actual tracking | Dict with cumulative metrics |
sensor.hsem_degraded_mode_sensor |
System Health | Overall system health | ok, degraded, error |
sensor.hsem_ev_charger_calculated_power |
EV Charger Calculated Power | Planner target power for primary EV charger | Watts (W) |
sensor.hsem_ev_second_charger_calculated_power |
EV 2 Charger Calculated Power | Planner target power for second EV charger | Watts (W) |
sensor.hsem_ev_charger_current_limit |
EV Charger Current Limit | Planner charging ceiling for primary EV charger | Amps (A) |
sensor.hsem_ev_second_charger_current_limit |
EV 2 Charger Current Limit | Planner charging ceiling for second EV charger | Amps (A) |
sensor.hsem_ev_charging_sensor |
EV Charging Active | Any EV actively charging | on, off |
sensor.hsem_ev_optimal_charging_plan |
EV Optimal Charging Plan | Primary EV plan state | charging, waiting, etc. |
sensor.hsem_ev_second_optimal_charging_plan |
EV Second Optimal Charging Plan | Second EV plan state | charging, waiting, etc. |
sensor.hsem_force_mode_sensor |
Force Working Mode | Override active indicator | auto or override mode name |
sensor.hsem_solar_confidence_sensor |
Solar Forecast Confidence | Per-hour PV forecast accuracy factors | Mean factor (ratio) |
sensor.hsem_hardware_writes_sensor |
Hardware Writes | Writes allowed/blocked by safety gate | allowed, blocked |
sensor.hsem_read_only_sensor |
Read-Only Mode | Read-only mode indicator | on, off |
sensor.hsem_net_consumption_sensor |
Net Consumption | Net load (house minus solar) | Watts (W) |
sensor.hsem_last_updated_sensor |
Last Updated | Last coordinator cycle timestamp | ISO-8601 timestamp |
sensor.hsem_next_update_sensor |
Next Update | Next scheduled coordinator cycle | ISO-8601 timestamp |
sensor.hsem_missing_entities_sensor |
Missing Input Entities | Count of missing input entities | Integer |
sensor.hsem_plan_explanation_sensor |
Plan Explanation | Planner strategy and cost breakdown | Winning candidate name |
sensor.hsem_prediction_accuracy_sensor |
Prediction Accuracy | Multi-horizon forecast accuracy | soc_mae_7d |
sensor.hsem_forecast_accuracy_sensor |
Forecast Accuracy | PV and load forecast MAE | kWh |
sensor.hsem_recommendation_interval_sensor |
Recommendation Interval | Slot width and horizon info | Minutes |
sensor.hsem_update_interval_sensor |
Update Interval | Current polling interval | Minutes |
sensor.hsem_workingmode_sensor |
Working Mode | Active battery recommendation | Working mode state |
sensor.hsem_export_income |
Export Income | Cumulative export revenue | Monetary (total) |
sensor.hsem_import_cost |
Import Cost | Cumulative import cost | Monetary (total) |
sensor.hsem_net_grid_balance |
Net Grid Balance | Export income minus import cost | Monetary (total) |
sensor.hsem_effective_discharge_floor_sensor |
Effective Discharge Floor | Current effective floor SoC | Percentage |
sensor.hsem_ocpp_charger_status_sensor |
OCPP Charger Status | Charger connection/charging state | String |
sensor.hsem_ocpp_charger_power_sensor |
OCPP Charger Power | Live charging power | kW |
sensor.hsem_ocpp_charger_info_sensor |
OCPP Charger Info | Vendor, model, firmware, serial | String |
sensor.hsem_ocpp_charger_sessions_sensor |
OCPP Charger Sessions | Completed session log | Integer |
sensor.hsem_savings_tracker_sensor |
Savings Tracker | Actual vs missed savings (90-day) | Monetary |
sensor.hsem_performance_savings_capture |
Performance: Savings Capture | Share of the achievable savings achieved (24 h) | Percentage |
sensor.hsem_performance_actual_benefit |
Performance: Actual Benefit | Benefit over plain self-consumption (24 h) | Local currency |
sensor.hsem_performance_missed_opportunity |
Performance: Missed Opportunity | What perfect hindsight would have added (24 h) | Local currency |
sensor.hsem_performance_battery_utilisation |
Performance: Battery Utilisation | Potential not lost through battery dispatch | Percentage |
sensor.hsem_performance_solar_utilisation |
Performance: Solar Utilisation | Potential not lost through PV surplus handling | Percentage |
sensor.hsem_performance_ev_charging_efficiency |
Performance: EV Charging Efficiency | Realized share of the best EV charging timing (EV planning only) | Percentage |
sensor.hsem_pv_curtailment_sensor |
PV Curtailment | PV curtailment detection | curtailed / normal |
Entity: select.hsem_force_working_mode
| Option | Description |
|---|---|
auto |
Normal operation — planner controls battery |
batteries_charge_grid |
Force grid charge |
batteries_charge_solar |
Force solar charge |
batteries_discharge_mode |
Force discharge to house |
batteries_discharge_window_mode |
Force discharge to house (window label) |
batteries_wait_mode |
Force idle |
ev_smart_charging |
Force EV charging |
force_batteries_discharge |
Force discharge to grid |
force_export |
Force all energy to export |
Entity: select.hsem_solcast_likelihood
Selects which Solcast likelihood scenario to use for PV forecasts (e.g. p10, p50, p90).
This setting is also configurable in the options flow.
EV switches are only created when the corresponding EV's planned load
(managed charging) is enabled — hsem_ev_planned_load_enabled for the
primary EV, hsem_ev_second_planned_load_enabled for the second EV
(issue #859). All other switches are always created.
| Entity | Purpose |
|---|---|
switch.hsem_read_only |
Block all hardware writes |
switch.hsem_extended_attributes |
Enable extended diagnostic attributes |
switch.hsem_verbose_logging |
Enable verbose logging |
switch.hsem_ev_charger_force_max_discharge_power |
Force EV maximum discharge power |
switch.hsem_ev_smart_charging |
Enable smart EV charging scheduling |
switch.hsem_ev_force_charge_now |
Force immediate EV charging |
switch.hsem_ev_second_smart_charging |
Enable smart charging for second EV |
switch.hsem_ev_second_force_charge_now |
Force immediate second EV charging |
switch.hsem_ml_consumption_enabled |
Enable ML-based consumption prediction |
switch.hsem_ml_consumption_sequential |
Enable sequential (intra-day momentum) ML mode |
switch.hsem_dynamic_discharge_floor |
Enable dynamic discharge floor |
switch.hsem_ev_auto_full_negative_price |
Auto-Full EV on negative price |
switch.hsem_ev_min_soc_charge_enabled |
Charge EV immediately below its minimum SoC |
switch.hsem_ev_second_min_soc_charge_enabled |
Charge second EV immediately below its minimum SoC |
switch.hsem_ev_cheap_price_charge_enabled |
Charge EV immediately at or below a price |
switch.hsem_ev_second_cheap_price_charge_enabled |
Charge second EV immediately at or below a price |
number.hsem_ev_target_soc, number.hsem_ev_min_soc_charge_soc and
number.hsem_ev_cheap_price_charge_max_price are only created when
hsem_ev_planned_load_enabled is set; the number.hsem_ev_second_*
versions only when hsem_ev_second_planned_load_enabled is set (issue #859). The battery
efficiency numbers are always created.
| Entity | Purpose | Range |
|---|---|---|
number.hsem_battery_charge_efficiency |
Battery charge efficiency | 1–100 % |
number.hsem_battery_discharge_efficiency |
Battery discharge efficiency | 1–100 % |
number.hsem_ev_target_soc |
Primary EV target SoC | 0–100 % |
number.hsem_ev_second_target_soc |
Second EV target SoC | 0–100 % |
number.hsem_ev_min_soc_charge_soc |
Primary EV minimum SoC (charge immediately below it) | 1–100 % |
number.hsem_ev_second_min_soc_charge_soc |
Second EV minimum SoC (charge immediately below it) | 1–100 % |
number.hsem_ev_cheap_price_charge_max_price |
Primary EV cheap-price threshold (import sensor's unit) | −10 to 10 |
number.hsem_ev_second_cheap_price_charge_max_price |
Second EV cheap-price threshold (import sensor's unit) | −10 to 10 |
time.hsem_ev_deadline_time is only created when hsem_ev_planned_load_enabled
is set; time.hsem_ev_second_deadline_time only when
hsem_ev_second_planned_load_enabled is set (issue #859).
| Entity | Purpose |
|---|---|
time.hsem_ev_deadline_time |
Primary EV charge deadline |
time.hsem_ev_second_deadline_time |
Second EV charge deadline |
The config flow includes a quick_setup step that auto-detects HA entities
(Huawei inverter, EV charger, Solcast forecasts, price sensors) to reduce
manual configuration.
OCPP configuration is exposed through the config flow with these keys:
hsem_ocpp_enabled, hsem_ocpp_port, hsem_ocpp_cpid,
hsem_ocpp_start_window_s, hsem_ocpp_stop_window_s. See
OCPP charger sensors above.
Logs the path to the bundled HSEM dashboard YAML and provides import instructions. The dashboard includes cards for working mode, battery SoC, financial sensors, EV status, and plan explanation. Import the YAML manually via Developer Tools → Services or Settings → Dashboards.
The service does not create or modify Home Assistant dashboards automatically; it only surfaces the bundled YAML path for manual import.
These changes are internal to the planner and do not expose new entities:
- Weekday/weekend profiling (#612):
WeekdayProfilemodule-level singleton distinguishes weekday vs weekend consumption patterns for more accurate load forecasting. - Session EV charging (#615):
EVConfig.session_charge_kwfield allows per-session charge power configuration for EV co-optimisation.
The data_quality dict on the working mode sensor is the serialized planner
input-completeness report. Example structure:
{
"is_complete": false,
"load_forecast_ready": false,
"load_forecast_reason": "zero_forecast_with_live_demand",
"load_forecast_missing_hours": [],
"load_forecast_estimated_hours": [],
"horizon_has_tomorrow": true,
"horizon_days": 2,
"tomorrow_price_missing_hours": [],
"tomorrow_pv_missing_hours": [],
"day2_price_missing_hours": [],
"day2_pv_missing_hours": [],
"today_price_missing_hours": [],
"today_pv_missing_hours": []
}The *_price_missing_hours and *_pv_missing_hours lists name the hours no
source covered (issue #1196). A missing price is planned with the same-hour
price of the nearest earlier day (the same quarter of the hour when prices are
quarter-hourly), a missing PV forecast with zero. With a 36 or
48 h horizon tomorrow_price_missing_hours is therefore filled, and
is_complete false, every day until the day-ahead prices are published; a
price forecast sensor that covers tomorrow closes the gap. PV hours are only
listed when a PV forecast sensor is configured. A published price or forecast
of exactly 0 is data and is not listed.
load_forecast_ready is false when average-sensor provenance or a populated
future value is missing, non-finite, or negative. A complete zero profile is
valid while live house demand is at most 50 W; above that threshold the reason
is zero_forecast_with_live_demand. is_complete requires both load readiness
and complete price/PV inputs.
load_forecast_missing_hours lists the hour blocks whose rolling-average
sensors have no stored sample yet. Up to 4 such hours are planned with a
conservative estimate (the larger of the nearest measured hours on each side)
and listed in load_forecast_estimated_hours. is_complete stays false
while any hour is estimated. With more missing hours the forecast is not
ready (source_unavailable) and the missing hours are still listed
(issue #1110).