From 09a6aec697d8391786f10685508e4cc1c76b354c Mon Sep 17 00:00:00 2001 From: ArticOdin Date: Sat, 16 May 2026 13:56:00 +1000 Subject: [PATCH] fix: UAT-found bugs (Phase 3.0 wizard rollout) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three bugs surfaced during the post-Phase-3.0d UAT walkthrough on a fresh-wiped HA install. None caught by the test suite — they're sensor-rendering-vs-coordinator-data shape mismatches that only manifest at runtime. 🟢 metrics_won returned fake "0/3" when Amber not configured - coordinator's `metrics_won = None` round-2 fix was correct, but `MetricsWonSensor.native_value`'s inline-compute fallback returned the literal string "0/3" when amber_import or current_plan_import was None. Now returns None — sensor renders "unavailable" honestly instead of fake-comparing against a phantom zero-cost provider. 🟢 Duplicate sensor entity sets for the user's current plan - Generic per-provider sensors (cost / import_rate / export_rate) were registered for the user's CURRENT plan AND comparators. The current plan already has hardcoded `current_plan_*` sensors, so the generic ones produced duplicates like `sensor.pricehawk_globird_zerohero_residential_flexible_rate_united_energy_cost_today`. sensor.py now skips the current plan in the providers loop; comparators (Amber, FlowPower, LocalVolts) keep their per-provider entities. 🟢 Flow Power default-OFF on new installs - Wizard defaulted `flow_power_enabled = True` regardless of user choice. Every install got a placeholder `sensor.pricehawk_flow_power_cost_today: $1.0` whether the user cared or not. Now opt-in: enabled only when user picks Flow Power as the primary at credentials, OR enables it via the comparators OptionsFlow step. Same default flipped in the comparators step schema. 623/623 non-pydantic tests pass; ruff clean. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.7 (1M context) --- custom_components/pricehawk/config_flow.py | 12 ++++++----- custom_components/pricehawk/sensor.py | 25 ++++++++++++++++++---- 2 files changed, 28 insertions(+), 9 deletions(-) diff --git a/custom_components/pricehawk/config_flow.py b/custom_components/pricehawk/config_flow.py index f98ae80..92238d0 100644 --- a/custom_components/pricehawk/config_flow.py +++ b/custom_components/pricehawk/config_flow.py @@ -1860,10 +1860,12 @@ async def async_step_dashboard_token( # Provider enables based on the primary choice amber_enabled = current_provider == PROVIDER_AMBER localvolts_enabled = current_provider == PROVIDER_LOCALVOLTS - # Flow Power is always on as a comparator. If the primary IS - # Flow Power, the region/base/supply were set at the - # credentials step; otherwise default to NSW1 / 34c / 100c. - flow_power_enabled = True + # Phase 3.0g (UAT): Flow Power default-OFF. Was forced ON + # under Phase 2 wizard (every install got a placeholder + # `flow_power_cost_today: $1.0` sensor whether the user + # cared or not). Comparators are now opt-in via the + # OptionsFlow comparators step. + flow_power_enabled = current_provider == PROVIDER_FLOW_POWER options: dict[str, Any] = { CONF_PLAN_TYPE: self._data.get(CONF_PLAN_TYPE, PLAN_ZEROHERO), @@ -2032,7 +2034,7 @@ async def async_step_comparators( ): bool, vol.Optional( CONF_FLOW_POWER_ENABLED, - default=current_opts.get(CONF_FLOW_POWER_ENABLED, True), + default=current_opts.get(CONF_FLOW_POWER_ENABLED, False), ): bool, vol.Optional( CONF_LOCALVOLTS_ENABLED, diff --git a/custom_components/pricehawk/sensor.py b/custom_components/pricehawk/sensor.py index 30db303..3aeab39 100644 --- a/custom_components/pricehawk/sensor.py +++ b/custom_components/pricehawk/sensor.py @@ -208,10 +208,15 @@ def __init__(self, coordinator: Any, entry: ConfigEntry) -> None: @property def native_value(self) -> str | None: + # Phase 3.0g (UAT): trust coordinator's None as "no comparison + # available" (e.g., Amber not configured). Don't synthesize a + # fake "0/3" — sensor renders "unavailable" instead, which + # honestly reflects the missing comparator. val = self.coordinator.data.get("metrics_won") if val is not None: return val - # Compute inline if coordinator doesn't provide it + # Inline-compute fallback for older coordinator data shapes + # (back-compat). Returns None when Amber isn't available. data = self.coordinator.data amber_import = data.get("amber_import_rate") current_plan_import = data.get("current_plan_import_rate") @@ -220,7 +225,7 @@ def native_value(self) -> str | None: amber_daily = data.get("amber_daily_cost") current_plan_daily = data.get("current_plan_daily_cost") if amber_import is None or current_plan_import is None: - return "0/3" + return None metrics = [ amber_import < current_plan_import, (amber_export or 0) > (current_plan_export or 0), @@ -540,10 +545,22 @@ async def async_setup_entry( entities.append(ZeroHeroStatusSensor(coordinator, entry)) # Generic per-provider sensors (pricehawk__*) — registered for - # every provider currently active in the coordinator. Reads the canonical - # data["providers"][] block. + # every comparator provider currently active in the coordinator. + # Phase 3.0g (UAT): SKIP the user's CURRENT plan provider — its + # rate/cost/kwh metrics already have hardcoded `current_plan_*` + # sensors registered above. Registering both produces duplicate + # entities (`sensor.pricehawk___*` vs + # `sensor.pricehawk_current_plan_*`). Comparators (Amber, Flow + # Power, LocalVolts) keep their per-provider entities. providers_block = coordinator.data.get("providers", {}) if coordinator.data else {} + current_plan_id = ( + coordinator._current_plan_provider.id + if hasattr(coordinator, "_current_plan_provider") + else None + ) for provider_id, snap in providers_block.items(): + if provider_id == current_plan_id: + continue provider_name = snap.get("name", provider_id.title()) entities.append( GenericProviderRateSensor(