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
12 changes: 7 additions & 5 deletions custom_components/pricehawk/config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down Expand Up @@ -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,
Expand Down
25 changes: 21 additions & 4 deletions custom_components/pricehawk/sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand All @@ -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),
Expand Down Expand Up @@ -540,10 +545,22 @@ async def async_setup_entry(
entities.append(ZeroHeroStatusSensor(coordinator, entry))

# Generic per-provider sensors (pricehawk_<provider>_*) — registered for
# every provider currently active in the coordinator. Reads the canonical
# data["providers"][<id>] 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_<brand>_<planid>_*` 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:
Comment on lines +556 to +562

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (bug_risk): Guard against _current_plan_provider being present but None, and double-check ID type consistency.

hasattr(coordinator, "_current_plan_provider") only checks for existence, so if the attribute exists but is None, coordinator._current_plan_provider.id will raise an AttributeError. Consider:

current_plan_provider = getattr(coordinator, "_current_plan_provider", None)
current_plan_id = getattr(current_plan_provider, "id", None)

Also, provider_id == current_plan_id may fail silently if their types differ (e.g., dict key vs model ID). If their types aren’t guaranteed to match, normalize before comparing (e.g., str(provider_id) == str(current_plan_id)).

continue
provider_name = snap.get("name", provider_id.title())
entities.append(
GenericProviderRateSensor(
Expand Down
Loading