348 files

This commit is contained in:
Home Assistant Version Control
2026-07-23 19:02:12 +00:00
parent 0ed1687503
commit 016af2e013
348 changed files with 12368 additions and 11689 deletions
+122 -24
View File
@@ -73,8 +73,10 @@ from .const import (
CONF_WATCHDOG_INTERVAL,
DEFAULT_DEVICE_TYPE,
DEFAULT_MAINTENANCE_REMINDER_CYCLES,
DEFAULT_MIN_POWER,
DEFAULT_OFF_DELAY,
DEFAULT_OFF_DELAY_BY_DEVICE,
DEFAULT_RUNNING_DEAD_ZONE,
DEVICE_TYPE_PUMP,
MAINTENANCE_EVENT_TYPES,
DEVICE_TYPES,
@@ -1838,7 +1840,9 @@ def ws_rebuild_envelopes(
entry_id, "rebuild", "Rebuilding envelopes",
label_key="task.rebuild.envelopes", label_params={},
)
hass.async_create_task(_rebuild_envelopes_task(hass, task, entry_id))
_raw = hass.async_create_task(_rebuild_envelopes_task(hass, task, entry_id))
if _raw is not None:
reg.link_asyncio_task(task.id, _raw)
_send_result(connection, msg["id"], "rebuild_envelopes", {"task_id": task.id})
@@ -1852,8 +1856,10 @@ async def _rebuild_envelopes_task(hass: HomeAssistant, task: Any, entry_id: str)
return
store = manager.profile_store
lock = _entry_write_lock(hass, entry_id)
await lock.acquire()
acquired = False
try:
await lock.acquire()
acquired = True
names = list(store.get_profiles().keys())
reg.update(task, total=len(names), done=0)
rebuilt = 0
@@ -1873,11 +1879,15 @@ async def _rebuild_envelopes_task(hass: HomeAssistant, task: Any, entry_id: str)
state=task_registry.STATE_CANCELLED if task.cancel_requested else task_registry.STATE_DONE,
result={"success": True, "rebuilt": rebuilt},
)
except asyncio.CancelledError:
reg.finish(task, state=task_registry.STATE_CANCELLED)
raise
except Exception as exc: # pylint: disable=broad-exception-caught
_LOGGER.warning("Rebuild-envelopes task failed for %s: %s", entry_id, exc)
reg.finish(task, state=task_registry.STATE_ERROR, error=str(exc))
finally:
lock.release()
if acquired:
lock.release()
@websocket_api.websocket_command(
@@ -2703,12 +2713,22 @@ async def _reprocess_task(hass: HomeAssistant, task: Any, entry_id: str) -> None
# retrains and rewrites the store, and must not interleave with a concurrent
# import / recording persist for the same entry.
lock = _entry_write_lock(hass, entry_id)
await lock.acquire()
acquired = False
try:
await lock.acquire()
acquired = True
if task.cancel_requested:
reg.finish(task, state=task_registry.STATE_CANCELLED)
return
reg.update(task, total=5, done=0, label="Reprocessing: matching cycles",
label_key="task.reprocess.matching")
summary["count"] = await store.async_reprocess_all_data()
if task.cancel_requested:
reg.finish(task, state=task_registry.STATE_CANCELLED, result=summary)
return
reg.update(task, done=1, label="Reprocessing: backfilling golden",
label_key="task.reprocess.golden")
try:
@@ -2716,6 +2736,10 @@ async def _reprocess_task(hass: HomeAssistant, task: Any, entry_id: str) -> None
except Exception as exc: # pylint: disable=broad-exception-caught
_LOGGER.debug("golden backfill failed for %s: %s", entry_id, exc)
if task.cancel_requested:
reg.finish(task, state=task_registry.STATE_CANCELLED, result=summary)
return
reg.update(task, done=2, label="Reprocessing: suggestions",
label_key="task.reprocess.suggestions")
learning = getattr(manager, "learning_manager", None)
@@ -2739,6 +2763,10 @@ async def _reprocess_task(hass: HomeAssistant, task: Any, entry_id: str) -> None
except Exception as exc: # pylint: disable=broad-exception-caught
_LOGGER.debug("ML training failed for %s: %s", entry_id, exc)
if task.cancel_requested:
reg.finish(task, state=task_registry.STATE_CANCELLED, result=summary)
return
reg.update(task, done=4, label="Reprocessing: cycle health",
label_key="task.reprocess.health")
# Recompute per-cycle health against the (possibly retrained) model.
@@ -2755,13 +2783,17 @@ async def _reprocess_task(hass: HomeAssistant, task: Any, entry_id: str) -> None
if _get_manager(hass, entry_id) is manager:
manager.notify_update()
reg.finish(task, state=task_registry.STATE_DONE, result=summary)
except asyncio.CancelledError:
reg.finish(task, state=task_registry.STATE_CANCELLED)
raise
except Exception as exc: # pylint: disable=broad-exception-caught
# Task-level failure: log at WARNING so it is visible in the default HA log
# and the panel Logs view (sub-step failures above stay at debug on purpose).
_LOGGER.warning("Reprocess task failed for %s: %s", entry_id, exc)
reg.finish(task, state=task_registry.STATE_ERROR, error=str(exc))
finally:
lock.release()
if acquired:
lock.release()
@websocket_api.websocket_command(
@@ -2783,7 +2815,9 @@ def ws_reprocess_history(
return
reg = task_registry.get_registry(hass)
task = reg.create(entry_id, "reprocess", "Reprocessing")
hass.async_create_task(_reprocess_task(hass, task, entry_id))
_raw = hass.async_create_task(_reprocess_task(hass, task, entry_id))
if _raw is not None:
reg.link_asyncio_task(task.id, _raw)
_send_result(connection, msg["id"], "reprocess_history", {"task_id": task.id})
@@ -3274,9 +3308,11 @@ def ws_trim_cycle(
task = reg.create(
entry_id, "trim", "Trimming cycle", label_key="task.trim.apply", label_params={},
)
hass.async_create_task(_trim_task(
_raw = hass.async_create_task(_trim_task(
hass, task, entry_id, msg["cycle_id"], float(msg["start_s"]), float(msg["end_s"]),
))
if _raw is not None:
reg.link_asyncio_task(task.id, _raw)
_send_result(connection, msg["id"], "trim_cycle", {"task_id": task.id})
@@ -3293,8 +3329,13 @@ async def _trim_task(
return
store = manager.profile_store
lock = _entry_write_lock(hass, entry_id)
await lock.acquire()
acquired = False
try:
await lock.acquire()
acquired = True
if task.cancel_requested:
reg.finish(task, state=task_registry.STATE_CANCELLED)
return
reg.update(task, total=1, done=0)
ok = await store.trim_cycle_power_data(cycle_id, start_s, end_s)
reg.update(task, done=1)
@@ -3304,11 +3345,15 @@ async def _trim_task(
if _get_manager(hass, entry_id) is manager:
manager.notify_update()
reg.finish(task, state=task_registry.STATE_DONE, result={"success": True})
except asyncio.CancelledError:
reg.finish(task, state=task_registry.STATE_CANCELLED)
raise
except Exception as exc: # pylint: disable=broad-exception-caught
_LOGGER.warning("Trim task failed for %s: %s", entry_id, exc)
reg.finish(task, state=task_registry.STATE_ERROR, error=str(exc))
finally:
lock.release()
if acquired:
lock.release()
@websocket_api.websocket_command(
@@ -3426,7 +3471,9 @@ def ws_apply_split(
task = reg.create(
entry_id, "split", "Splitting cycle", label_key="task.split.apply", label_params={},
)
hass.async_create_task(_apply_split_task(hass, task, entry_id, cycle_id, segments))
_raw = hass.async_create_task(_apply_split_task(hass, task, entry_id, cycle_id, segments))
if _raw is not None:
reg.link_asyncio_task(task.id, _raw)
_send_result(connection, msg["id"], "apply_split", {"task_id": task.id})
@@ -3444,8 +3491,13 @@ async def _apply_split_task(
return
store = manager.profile_store
lock = _entry_write_lock(hass, entry_id)
await lock.acquire()
acquired = False
try:
await lock.acquire()
acquired = True
if task.cancel_requested:
reg.finish(task, state=task_registry.STATE_CANCELLED)
return
reg.update(task, total=1, done=0)
new_ids = await store.apply_split_interactive(cycle_id, segments)
reg.update(task, done=1)
@@ -3455,9 +3507,15 @@ async def _apply_split_task(
task, state=task_registry.STATE_DONE,
result={"success": True, "new_ids": new_ids},
)
except asyncio.CancelledError:
reg.finish(task, state=task_registry.STATE_CANCELLED)
raise
except Exception as exc: # pylint: disable=broad-exception-caught
_LOGGER.warning("Apply-split task failed for %s: %s", entry_id, exc)
reg.finish(task, state=task_registry.STATE_ERROR, error=str(exc))
finally:
if acquired:
lock.release()
@websocket_api.websocket_command(
@@ -3508,7 +3566,9 @@ def ws_apply_merge(
task = reg.create(
entry_id, "merge", "Merging cycles", label_key="task.merge.apply", label_params={},
)
hass.async_create_task(_apply_merge_task(hass, task, entry_id, ids, target, new_name))
_raw = hass.async_create_task(_apply_merge_task(hass, task, entry_id, ids, target, new_name))
if _raw is not None:
reg.link_asyncio_task(task.id, _raw)
_send_result(connection, msg["id"], "apply_merge", {"task_id": task.id})
@@ -3526,8 +3586,13 @@ async def _apply_merge_task(
return
store = manager.profile_store
lock = _entry_write_lock(hass, entry_id)
await lock.acquire()
acquired = False
try:
await lock.acquire()
acquired = True
if task.cancel_requested:
reg.finish(task, state=task_registry.STATE_CANCELLED)
return
reg.update(task, total=1, done=0)
created_new = False
if new_name:
@@ -3553,11 +3618,15 @@ async def _apply_merge_task(
task, state=task_registry.STATE_DONE,
result={"success": True, "new_id": new_id},
)
except asyncio.CancelledError:
reg.finish(task, state=task_registry.STATE_CANCELLED)
raise
except Exception as exc: # pylint: disable=broad-exception-caught
_LOGGER.warning("Apply-merge task failed for %s: %s", entry_id, exc)
reg.finish(task, state=task_registry.STATE_ERROR, error=str(exc))
finally:
lock.release()
if acquired:
lock.release()
# ─── Profile envelope / member cycles ──────────────────────────────────────────
@@ -4620,17 +4689,23 @@ async def _ml_training_task(hass: HomeAssistant, task: Any, entry_id: str) -> No
# reprocess itself runs ML training): training rewrites the store, so two runs
# for the same entry must not interleave.
lock = _entry_write_lock(hass, entry_id)
await lock.acquire()
acquired = False
try:
await lock.acquire()
acquired = True
summary = await manager.async_run_ml_training(force=True)
reg.finish(task, state=task_registry.STATE_DONE, result=summary)
except asyncio.CancelledError:
reg.finish(task, state=task_registry.STATE_CANCELLED)
raise
except Exception as exc: # pylint: disable=broad-exception-caught
# Task-level failure: log at WARNING so it surfaces in the default HA log and
# the panel Logs view (not swallowed at debug like a routine sub-step miss).
_LOGGER.warning("ML training task failed for %s: %s", entry_id, exc)
reg.finish(task, state=task_registry.STATE_ERROR, error=str(exc))
finally:
lock.release()
if acquired:
lock.release()
@websocket_api.websocket_command(
@@ -4656,7 +4731,9 @@ def ws_trigger_ml_training(
return
reg = task_registry.get_registry(hass)
task = reg.create(entry_id, "ml_training", "Learning")
hass.async_create_task(_ml_training_task(hass, task, entry_id))
_raw = hass.async_create_task(_ml_training_task(hass, task, entry_id))
if _raw is not None:
reg.link_asyncio_task(task.id, _raw)
_send_result(connection, msg["id"], "trigger_ml_training", {"task_id": task.id})
@@ -4838,7 +4915,7 @@ def _playground_base_config(manager: Any, entry: Any) -> CycleDetectorConfig:
opts: dict[str, Any] = {}
if entry is not None:
opts = {**getattr(entry, "data", {}), **getattr(entry, "options", {})}
min_power = float(opts.get(CONF_MIN_POWER, 5.0) or 5.0)
min_power = float(opts.get(CONF_MIN_POWER, DEFAULT_MIN_POWER) or DEFAULT_MIN_POWER)
return CycleDetectorConfig(
min_power=min_power,
off_delay=int(opts.get(CONF_OFF_DELAY, DEFAULT_OFF_DELAY)),
@@ -4846,7 +4923,7 @@ def _playground_base_config(manager: Any, entry: Any) -> CycleDetectorConfig:
completion_min_seconds=int(opts.get(CONF_COMPLETION_MIN_SECONDS, 600)),
end_repeat_count=int(opts.get(CONF_END_REPEAT_COUNT, 1)),
min_off_gap=int(opts.get(CONF_MIN_OFF_GAP, 60)),
running_dead_zone=int(opts.get(CONF_RUNNING_DEAD_ZONE, 0)),
running_dead_zone=int(opts.get(CONF_RUNNING_DEAD_ZONE, DEFAULT_RUNNING_DEAD_ZONE)),
start_threshold_w=float(opts.get(CONF_START_THRESHOLD_W, min_power)),
stop_threshold_w=float(
opts.get(CONF_STOP_THRESHOLD_W, min_power * 0.6 if min_power else 2.0)
@@ -5186,6 +5263,9 @@ async def _pg_history_task(
else:
ids = [c.get("id") for c in past[-playground.DEFAULT_RECENT_CYCLES:]]
ids = [i for i in ids[:playground.MAX_BATCH_CYCLES] if i]
# Build match snapshots once — they are store-derived and identical for
# every chunk; rebuilding per chunk is O(n_profiles) wasted work.
prebuilt = await hass.async_add_executor_job(playground._build_match_snapshots, store)
reg.update(task, total=len(ids))
rows: list[dict[str, Any]] = []
base_rows: list[dict[str, Any]] = []
@@ -5195,7 +5275,7 @@ async def _pg_history_task(
chunk = ids[i:i + _PG_HISTORY_CHUNK]
r = await hass.async_add_executor_job(
playground.run_playground_history,
store, chunk, base_config, override, options, price, len(chunk),
store, chunk, base_config, override, options, price, len(chunk), prebuilt,
)
rows.extend(r.get("rows") or [])
base_rows.extend(r.get("baseline_rows") or [])
@@ -5207,6 +5287,9 @@ async def _pg_history_task(
state=task_registry.STATE_CANCELLED if task.cancel_requested else task_registry.STATE_DONE,
result=payload,
)
except asyncio.CancelledError:
reg.finish(task, state=task_registry.STATE_CANCELLED)
raise
except Exception as exc: # pylint: disable=broad-exception-caught
_LOGGER.debug("Playground history task failed for %s: %s", entry_id, exc)
reg.finish(task, state=task_registry.STATE_ERROR, error=str(exc))
@@ -5228,6 +5311,8 @@ async def _pg_sweep_task(
ids = [c.get("id") for c in past[-playground.DEFAULT_RECENT_CYCLES:] if isinstance(c, dict)]
ids = [i for i in ids[:playground.MAX_BATCH_CYCLES] if i]
n = max(1, len(ids))
# Build match snapshots once — identical for every sweep value/cell.
prebuilt = await hass.async_add_executor_job(playground._build_match_snapshots, store)
if param_y and values_y:
reg.update(task, total=len(values) * len(values_y))
grid: list[list[float | None]] = [[None] * len(values) for _ in values_y]
@@ -5242,7 +5327,7 @@ async def _pg_sweep_task(
r = await hass.async_add_executor_job(
playground.run_playground_sweep,
store, ids, base_config, param, [vx], objective,
options, price, n, param_y, [vy],
options, price, n, param_y, [vy], prebuilt,
)
cell = (r.get("grid") or [[None]])[0]
grid[j][i] = cell[0] if cell else None
@@ -5263,6 +5348,7 @@ async def _pg_sweep_task(
r = await hass.async_add_executor_job(
playground.run_playground_sweep,
store, ids, base_config, param, [vx], objective, options, price, n,
None, None, prebuilt,
)
points.extend(r.get("points") or [])
if r.get("current_value") is not None:
@@ -5275,6 +5361,9 @@ async def _pg_sweep_task(
state=task_registry.STATE_CANCELLED if task.cancel_requested else task_registry.STATE_DONE,
result=payload,
)
except asyncio.CancelledError:
reg.finish(task, state=task_registry.STATE_CANCELLED)
raise
except Exception as exc: # pylint: disable=broad-exception-caught
_LOGGER.debug("Playground sweep task failed for %s: %s", entry_id, exc)
reg.finish(task, state=task_registry.STATE_ERROR, error=str(exc))
@@ -5304,7 +5393,9 @@ def ws_start_playground_history(
task = reg.create(entry_id, "pg_history", "Test on history")
override = dict(msg.get("settings_override") or {}) or None
cycle_ids = list(msg.get("cycle_ids") or [])
hass.async_create_task(_pg_history_task(hass, task, entry_id, cycle_ids, override))
_raw = hass.async_create_task(_pg_history_task(hass, task, entry_id, cycle_ids, override))
if _raw is not None:
reg.link_asyncio_task(task.id, _raw)
_send_result(connection, msg["id"], "start_playground_history", {"task_id": task.id})
@@ -5340,10 +5431,12 @@ def ws_start_playground_sweep(
entry_id, "pg_sweep", f"Optimize: {msg['param']}",
label_key="task.pg_sweep.optimize", label_params={"param": msg["param"]},
)
hass.async_create_task(_pg_sweep_task(
_raw = hass.async_create_task(_pg_sweep_task(
hass, task, entry_id, msg["param"], list(msg.get("values") or []),
msg["objective"], param_y, list(values_y) if values_y else None,
))
if _raw is not None:
reg.link_asyncio_task(task.id, _raw)
_send_result(connection, msg["id"], "start_playground_sweep", {"task_id": task.id})
@@ -5393,6 +5486,9 @@ async def _pg_detail_task(
state=task_registry.STATE_CANCELLED if task.cancel_requested else task_registry.STATE_DONE,
result=payload,
)
except asyncio.CancelledError:
reg.finish(task, state=task_registry.STATE_CANCELLED)
raise
except Exception as exc: # pylint: disable=broad-exception-caught
_LOGGER.debug("Playground detail task failed for %s: %s", entry_id, exc)
reg.finish(task, state=task_registry.STATE_ERROR, error=str(exc))
@@ -5426,7 +5522,9 @@ def ws_start_playground_cycle_detail(
label_key="task.pg_detail.simulate", label_params={},
)
override = dict(msg.get("settings_override") or {})
hass.async_create_task(
_raw = hass.async_create_task(
_pg_detail_task(hass, task, entry_id, msg["cycle_id"], override)
)
if _raw is not None:
reg.link_asyncio_task(task.id, _raw)
_send_result(connection, msg["id"], "start_playground_cycle_detail", {"task_id": task.id})