diff --git a/custom_components/pricehawk/__init__.py b/custom_components/pricehawk/__init__.py index 678a71d..c963e90 100644 --- a/custom_components/pricehawk/__init__.py +++ b/custom_components/pricehawk/__init__.py @@ -41,6 +41,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: # Schedule periodic state persistence coordinator.schedule_persist() + # Phase 3.1 — schedule daily multi-plan ranking job at 00:30 local. + # First run also fires immediately so the alternatives sensor isn't + # empty until midnight on a fresh install. + coordinator.schedule_daily_ranking() + hass.async_create_task(coordinator.async_run_ranking_job()) + # Copy www assets (icon + HTML) and register sidebar panel await copy_www_assets(hass) await setup_panel_iframe(hass, entry) @@ -179,6 +185,34 @@ async def handle_backfill(call: object) -> None: hass.services.async_register(DOMAIN, "backfill_history", handle_backfill) + # Phase 3.1 commit 5 — manual ranking trigger. Lets users force-run + # the ranking pipeline from Developer Tools → Services without + # waiting for the next 00:30 schedule fire. Most useful right after + # switching plans (so the alternatives ranking reflects the new + # distributor / postcode immediately). + async def handle_rank_alternatives(call: object) -> None: + # CR-fix: malformed service payload (e.g. ``top_k: "abc"`` from + # a typo in a YAML automation) would raise ValueError/TypeError + # and fail the call. Coerce defensively + fall back to default. + raw = call.data.get("top_k", 20) # type: ignore[attr-defined] + try: + top_k = int(raw) + except (TypeError, ValueError): + _LOGGER.warning( + "rank_alternatives: invalid top_k=%r, using default 20", raw + ) + top_k = 20 + top_k = max(1, min(top_k, 100)) + result = await coordinator.async_run_ranking_job(top_k=top_k) + _LOGGER.info( + "rank_alternatives service: ran successfully, %d result(s)", + len(result), + ) + + hass.services.async_register( + DOMAIN, "rank_alternatives", handle_rank_alternatives + ) + _LOGGER.info("PriceHawk integration setup complete") return True @@ -191,6 +225,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) if coordinator: coordinator.cancel_persist() + coordinator.cancel_ranking() await coordinator.async_persist_state() await remove_panel(hass) @@ -199,5 +234,6 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: if not hass.data.get(DOMAIN): hass.services.async_remove(DOMAIN, "analyze_csv") hass.services.async_remove(DOMAIN, "backfill_history") + hass.services.async_remove(DOMAIN, "rank_alternatives") return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) diff --git a/custom_components/pricehawk/services.yaml b/custom_components/pricehawk/services.yaml index 92ec789..e4df2b6 100644 --- a/custom_components/pricehawk/services.yaml +++ b/custom_components/pricehawk/services.yaml @@ -30,3 +30,24 @@ backfill_history: min: 1 max: 90 mode: slider + +rank_alternatives: + name: Rank Alternative Plans + description: >- + Run the cheap-rank pipeline against your current retailer + competitor + retailers (AGL, Origin, EnergyAustralia, Red Energy). Results stored on + the coordinator and exposed via the alternatives sensor (Phase 3.1 + commit 6). Cheap-rank only for now; deep-rank by HA consumption replay + arrives in Phase 3.2. + fields: + top_k: + name: Top K + description: Number of cheapest alternatives to keep (default 20) + required: false + default: 20 + example: 20 + selector: + number: + min: 1 + max: 100 + mode: slider