Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 17 additions & 2 deletions .github/memories.md
Original file line number Diff line number Diff line change
Expand Up @@ -1517,8 +1517,23 @@ arrives.
`models/ocpp_session.py`. This repository ships OCPP as a supported product
feature, so removing it is a user-facing breaking change with no upstream
benefit. Treat OCPP's continued presence as a deliberate divergence, never as an
unfinished port. The same holds for fixed battery schedules. Only the dead
seven-bucket charge-rate learner from #7 was taken.
unfinished port. Only the dead seven-bucket charge-rate learner from #7 was taken.

**Fixed battery schedules — reversed 2026-08-31 (issue #860).** The
"keep fixed battery schedules" half of the line above no longer holds. Issue
#860 found the feature (`batteries_schedule_1/2/3` config, switches, time
entities, `BatterySchedule`/`BatteryScheduleInput`, and the
`apply_discharge_schedules`/`apply_charge_schedules`/`apply_arbitrage_grid_charge`
passes in `engine_core.py`) functionally inert whenever MILP is active — the
schedule-consuming heuristic candidates were commented out in MILP-only mode
(later deleted entirely in issue #897, since MILP is the sole active
optimisation authority), and both surviving candidates (`no_action`,
`passive`) discard schedule-derived recommendations before scoring. The
user explicitly chose full removal over
re-wiring it as a MILP-unavailable fallback or leaving it documented as inert,
consciously overriding the earlier "keep it" precedent. Do not resurrect
battery-schedule config/entities/code from this history as if it were still
the settled decision — check the removal PR for the current state instead.

**One OCPP server per EV (2026-08-23).** Each EV gets its own embedded OCPP
server on its own port (defaults 9000 / 9001): the primary plan drives the
Expand Down
32 changes: 1 addition & 31 deletions custom_components/hsem/config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,6 @@
get_batteries_excess_export_step_schema,
validate_batteries_excess_export_input,
)
from custom_components.hsem.flows.batteries_schedules import (
get_batteries_schedules_step_schema,
validate_batteries_schedules_input,
)
from custom_components.hsem.flows.batteries_wait_mode import (
get_batteries_wait_mode_step_schema,
validate_batteries_wait_mode_input,
Expand Down Expand Up @@ -587,7 +583,7 @@ async def async_step_ocpp(
errors = await validate_ocpp_step_input(self.hass, user_input)
if not errors:
self._user_input.update(user_input)
return await self.async_step_batteries_schedules()
return await self.async_step_batteries_wait_mode()

data_schema = await get_ocpp_step_schema(
None,
Expand All @@ -601,32 +597,6 @@ async def async_step_ocpp(
last_step=False,
)

async def async_step_batteries_schedules(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle the batteries_schedules config flow step.

Validates user input and advances to the next step in the config flow.
"""
errors = {}

if user_input is not None:
errors = await validate_batteries_schedules_input(user_input)
if not errors:
self._user_input.update(user_input)
return await self.async_step_batteries_wait_mode()

data_schema = await get_batteries_schedules_step_schema(
None, hass=self.hass, user_input=self._user_input
)

return self.async_show_form(
step_id="batteries_schedules",
data_schema=data_schema,
errors=errors,
last_step=False,
)

async def async_step_batteries_wait_mode(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
Expand Down
9 changes: 0 additions & 9 deletions custom_components/hsem/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,15 +31,6 @@
"hsem_batteries_expected_cycles": 6000,
"hsem_batteries_cycle_cost": 0.0,
"hsem_batteries_capacity_loss_pct": 30,
"hsem_batteries_enable_batteries_schedule_1_end": "09:00:00",
"hsem_batteries_enable_batteries_schedule_1_start": "07:00:00",
"hsem_batteries_enable_batteries_schedule_1": True,
"hsem_batteries_enable_batteries_schedule_2_end": "21:00:00",
"hsem_batteries_enable_batteries_schedule_2_start": "17:00:00",
"hsem_batteries_enable_batteries_schedule_2": True,
"hsem_batteries_enable_batteries_schedule_3_end": "02:00:00",
"hsem_batteries_enable_batteries_schedule_3_start": "23:00:00",
"hsem_batteries_enable_batteries_schedule_3": False,
"hsem_ev_target_soc": 80,
"hsem_ev_second_target_soc": 80,
"hsem_ev_deadline_time": "07:00",
Expand Down
3 changes: 0 additions & 3 deletions custom_components/hsem/coordinator.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,6 @@
from custom_components.hsem.custom_sensors.ocpp_server import OCPPServer
from custom_components.hsem.custom_sensors.state_collector import ( # noqa: F401 — kept for backward compat
async_collect_all_states,
build_battery_schedules,
build_sensor_config,
)
from custom_components.hsem.models.daily_plan_vs_actual_tracker import (
Expand Down Expand Up @@ -185,8 +184,6 @@ def __init__(self, hass: HomeAssistant, config_entry: ConfigEntry) -> None:
self._snapshot: StateSnapshot | None = None
self._hourly_recommendations: list[HourlyRecommendation] = []
self._hourly_recommendation: HourlyRecommendation | None = None
self._batteries_schedules: list = []
self._batteries_schedules_remaining_capacity_needed: float = 0.0
self._current_required_battery: float = 0.0
self._current_wait_mode_reserve: float | None = None
self._next_update: str | None = None
Expand Down
13 changes: 0 additions & 13 deletions custom_components/hsem/coordinator_builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@
import math
from datetime import datetime, timedelta

from custom_components.hsem.models.battery_schedule_input import BatteryScheduleInput
from custom_components.hsem.models.hourly_consumption_average import (
HourlyConsumptionAverage,
)
Expand Down Expand Up @@ -129,7 +128,6 @@ def build_planner_input(
cfg: SensorConfig,
live: LiveState,
hourly_recommendations: list[HourlyRecommendation],
batteries_schedules: list,
previous_winner_name: str | None,
previous_winner_score: float,
ev_session_kw: dict[str, float] | None = None,
Expand All @@ -148,7 +146,6 @@ def build_planner_input(
live: Live HA entity state snapshot.
hourly_recommendations: Recommendation slots generated by
:func:`generate_recommendation_intervals`.
batteries_schedules: Parsed battery schedule objects.
previous_winner_name: Winning candidate name from the previous
planner run, or ``None`` for the first run.
previous_winner_score: Score of the winning candidate from the
Expand Down Expand Up @@ -224,15 +221,6 @@ def build_planner_input(
)
)

battery_schedules = [
BatteryScheduleInput(
enabled=s.enabled,
start=s.start,
end=s.end,
)
for s in batteries_schedules
]

_cycles = convert_to_int(cfg.batteries_expected_cycles)
_w1d = convert_to_int(cfg.house_consumption_energy_weight_1d)
_w3d = convert_to_int(cfg.house_consumption_energy_weight_3d)
Expand Down Expand Up @@ -315,7 +303,6 @@ def build_planner_input(
consumption_averages=consumption_averages,
price_points=price_points,
solcast_slots=solcast_slots,
battery_schedules=battery_schedules,
excess_export_enabled=bool(cfg.batteries_enable_excess_export),
excess_export_discharge_buffer_pct=(
excess_export_buffer_pct if excess_export_buffer_pct is not None else 10.0
Expand Down
15 changes: 3 additions & 12 deletions custom_components/hsem/coordinator_cycle.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,6 @@
)
from custom_components.hsem.custom_sensors.state_collector import ( # noqa: F401 — kept for backward compat
async_collect_all_states,
build_battery_schedules,
build_sensor_config,
)
from custom_components.hsem.models.live_state import EVLiveState, LiveState
Expand Down Expand Up @@ -171,11 +170,7 @@ async def _async_collect_and_populate(
cfg.recommendation_interval_length,
)

# 4. Build battery-schedule objects from config.
self._batteries_schedules = build_battery_schedules(cfg)
self._batteries_schedules.sort(key=lambda x: x.start)

# 5. Populate weighted house-consumption averages.
# 4. Populate weighted house-consumption averages.
set_hsem_verbose(cfg.verbose_logging)

if cfg.ml_consumption_enabled:
Expand Down Expand Up @@ -298,7 +293,7 @@ async def _async_collect_and_populate(
else:
await self._set_update_interval()

# 6. Determine working state: forced, missing, or full pipeline.
# 5. Determine working state: forced, missing, or full pipeline.
state: str | None = None

if live.missing_entities and live.force_working_mode_state == "auto":
Expand All @@ -314,7 +309,7 @@ async def _async_collect_and_populate(
live.force_working_mode_state,
)

# 7. Populate electricity prices and Solcast PV estimates.
# 6. Populate electricity prices and Solcast PV estimates.
populate_price_and_solcast_from_snapshot(
self._hourly_recommendations,
self._snapshot,
Expand Down Expand Up @@ -602,10 +597,6 @@ async def _async_run_update_cycle(self) -> None:
live=self._live,
hourly_recommendations=list(self._hourly_recommendations),
hourly_recommendation=self._hourly_recommendation,
batteries_schedules=list(self._batteries_schedules),
batteries_schedules_remaining_capacity_needed=(
self._batteries_schedules_remaining_capacity_needed
),
current_required_battery=self._current_required_battery,
current_wait_mode_reserve=self._current_wait_mode_reserve,
state=state,
Expand Down
5 changes: 0 additions & 5 deletions custom_components/hsem/coordinator_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,6 @@ class CoordinatorData:
hourly_recommendations: Full list of planner recommendation slots.
hourly_recommendation: The recommendation slot active *right now*, or
``None`` when no matching slot exists.
batteries_schedules: Parsed battery charge/discharge schedule windows.
batteries_schedules_remaining_capacity_needed: Total remaining capacity
needed across all enabled battery schedules (kWh).
current_required_battery: Required battery capacity from the planner (kWh),
derived from ``calculate_required_battery_until_solar``. Used for
excess-export scheduling and the EV discharge-cap SoC guard.
Expand All @@ -55,8 +52,6 @@ class CoordinatorData:
live: LiveState | None = None
hourly_recommendations: list[HourlyRecommendation] = field(default_factory=list)
hourly_recommendation: HourlyRecommendation | None = None
batteries_schedules: list = field(default_factory=list)
batteries_schedules_remaining_capacity_needed: float = 0.0
current_required_battery: float = 0.0
current_wait_mode_reserve: float | None = None
state: str | None = None
Expand Down
4 changes: 0 additions & 4 deletions custom_components/hsem/coordinator_lifecycle.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@
from custom_components.hsem.custom_sensors.ocpp_server import OCPPServer
from custom_components.hsem.custom_sensors.state_collector import ( # noqa: F401 — kept for backward compat
async_collect_all_states,
build_battery_schedules,
build_sensor_config,
)
from custom_components.hsem.models.live_state import LiveState
Expand Down Expand Up @@ -507,9 +506,6 @@ def _apply_planner_output(self, output: PlannerOutput) -> None:
unmatched[0],
)

self._batteries_schedules_remaining_capacity_needed = sum(
s.needed_batteries_capacity for s in self._batteries_schedules if s.enabled
)
# Preserve the plan explanation and data quality for the next CoordinatorData snapshot.
self._plan_explanation = output.explanation
self._data_quality = output.data_quality
Expand Down
2 changes: 0 additions & 2 deletions custom_components/hsem/coordinator_planner_phase.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,6 @@
)
from custom_components.hsem.custom_sensors.state_collector import ( # noqa: F401 — kept for backward compat
async_collect_all_states,
build_battery_schedules,
build_sensor_config,
)
from custom_components.hsem.models.live_state import LiveState
Expand Down Expand Up @@ -161,7 +160,6 @@ async def _run_planner_phase(
cfg=cfg,
live=live,
hourly_recommendations=self._hourly_recommendations,
batteries_schedules=self._batteries_schedules,
previous_winner_name=self._previous_planner_winner_name,
previous_winner_score=self._previous_planner_winner_score,
ev_session_kw=ev_session_kw if ev_session_kw else None,
Expand Down
3 changes: 0 additions & 3 deletions custom_components/hsem/coordinator_state.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@
from custom_components.hsem.custom_sensors.ocpp_server import OCPPServer
from custom_components.hsem.custom_sensors.state_collector import ( # noqa: F401 — kept for backward compat
async_collect_all_states,
build_battery_schedules,
build_sensor_config,
)
from custom_components.hsem.models.daily_plan_vs_actual_tracker import (
Expand Down Expand Up @@ -69,8 +68,6 @@ class CoordinatorSharedState(_Base):
"""Type-only declaration of state shared across the coordinator mixins."""

_avg_house_consumption_entity_id_cache: dict[str, str]
_batteries_schedules: list
_batteries_schedules_remaining_capacity_needed: float
_capacity_learner: CapacityLearner
_cfg: SensorConfig
_config_entry: ConfigEntry
Expand Down
72 changes: 0 additions & 72 deletions custom_components/hsem/custom_sensors/config_reader.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,11 @@

from __future__ import annotations

from datetime import time
from typing import Any, cast

import voluptuous as vol

from custom_components.hsem.models.battery_schedule import BatterySchedule
from custom_components.hsem.models.sensor_config import (
BatteryScheduleConfig,
EVChargerConfig,
SensorConfig,
)
Expand All @@ -27,7 +24,6 @@
convert_to_boolean,
convert_to_float,
convert_to_int,
convert_to_time,
)
from custom_components.hsem.utils.misc import get_config_value
from custom_components.hsem.utils.phase_power import (
Expand Down Expand Up @@ -375,47 +371,6 @@ def build_sensor_config(
or 30.0
)

# Battery schedules
_s1_start = get_config_value(
config_entry, "hsem_batteries_enable_batteries_schedule_1_start"
)
_s1_end = get_config_value(
config_entry, "hsem_batteries_enable_batteries_schedule_1_end"
)
_s2_start = get_config_value(
config_entry, "hsem_batteries_enable_batteries_schedule_2_start"
)
_s2_end = get_config_value(
config_entry, "hsem_batteries_enable_batteries_schedule_2_end"
)
_s3_start = get_config_value(
config_entry, "hsem_batteries_enable_batteries_schedule_3_start"
)
_s3_end = get_config_value(
config_entry, "hsem_batteries_enable_batteries_schedule_3_end"
)
cfg.batteries_schedule_1 = BatteryScheduleConfig(
enabled=convert_to_boolean(
get_config_value(config_entry, "hsem_batteries_enable_batteries_schedule_1")
),
start=convert_to_time(_s1_start) if _s1_start is not None else None,
end=convert_to_time(_s1_end) if _s1_end is not None else None,
)
cfg.batteries_schedule_2 = BatteryScheduleConfig(
enabled=convert_to_boolean(
get_config_value(config_entry, "hsem_batteries_enable_batteries_schedule_2")
),
start=convert_to_time(_s2_start) if _s2_start is not None else None,
end=convert_to_time(_s2_end) if _s2_end is not None else None,
)
cfg.batteries_schedule_3 = BatteryScheduleConfig(
enabled=convert_to_boolean(
get_config_value(config_entry, "hsem_batteries_enable_batteries_schedule_3")
),
start=convert_to_time(_s3_start) if _s3_start is not None else None,
end=convert_to_time(_s3_end) if _s3_end is not None else None,
)

# Excess export
cfg.batteries_enable_excess_export = bool(
get_config_value(config_entry, "hsem_batteries_enable_excess_export")
Expand Down Expand Up @@ -646,33 +601,6 @@ def build_sensor_config(
return cfg


def build_battery_schedules(cfg: SensorConfig) -> list[BatterySchedule]:
"""Convert the three :class:`BatteryScheduleConfig` objects into :class:`BatterySchedule` instances.

Args:
cfg: Populated sensor configuration.

Returns:
A list of three :class:`BatterySchedule` objects (always three, regardless
of whether they are enabled).
"""
_midnight = time(0, 0) # safe fallback for unconfigured schedules
schedules = []
for sc in cfg.schedule_configs():
schedules.append(
BatterySchedule(
enabled=sc.enabled,
# start/end are time|None in BatteryScheduleConfig (optional schedule);
# BatterySchedule requires time, so fall back to midnight when not set.
start=sc.start if sc.start is not None else _midnight,
end=sc.end if sc.end is not None else _midnight,
avg_import_price=0.0,
needed_batteries_capacity=0.0,
)
)
return schedules


# ---------------------------------------------------------------------------
# Private helpers
# ---------------------------------------------------------------------------
Expand Down
Loading
Loading