Skip to content

Commit f07931e

Browse files
authored
Fix site-only setup and site energy entities (#184)
1 parent f5603b5 commit f07931e

File tree

5 files changed

+49
-34
lines changed

5 files changed

+49
-34
lines changed

custom_components/enphase_ev/config_flow.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -176,8 +176,10 @@ async def async_step_devices(
176176
user_input.get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL)
177177
)
178178
site_only_selected = bool(user_input.get(CONF_SITE_ONLY, False))
179-
selected = self._normalize_serials(serials)
180-
if selected or (site_only_selected and site_only_available):
179+
selected = [] if site_only_selected else self._normalize_serials(serials)
180+
if (selected and not site_only_selected) or (
181+
site_only_selected and site_only_available
182+
):
181183
self._site_only = site_only_selected
182184
return await self._finalize_login_entry(
183185
selected, scan_interval, site_only_selected
@@ -207,7 +209,9 @@ async def async_step_devices(
207209
schema = vol.Schema(
208210
{
209211
vol.Optional(CONF_SITE_ONLY, default=site_only_selected): bool,
210-
vol.Required(CONF_SERIALS): selector({"text": {"multiline": True}}),
212+
vol.Optional(CONF_SERIALS, default=""): selector(
213+
{"text": {"multiline": True}}
214+
),
211215
vol.Optional(CONF_SCAN_INTERVAL, default=default_scan): int,
212216
}
213217
)

custom_components/enphase_ev/coordinator.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2434,6 +2434,8 @@ def _ensure_serial_tracked(self, serial: str) -> bool:
24342434

24352435
def iter_serials(self) -> list[str]:
24362436
"""Return charger serials in a stable order for entity setup."""
2437+
if getattr(self, "site_only", False):
2438+
return []
24372439
ordered: list[str] = []
24382440
serial_order = getattr(self, "_serial_order", None)
24392441
known_serials = getattr(self, "serials", None)

custom_components/enphase_ev/sensor.py

Lines changed: 9 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -35,22 +35,24 @@ async def async_setup_entry(
3535
coord: EnphaseCoordinator = hass.data[DOMAIN][entry.entry_id]["coordinator"]
3636

3737
# Site-level diagnostic sensors
38-
site_entities = [
38+
site_entities: list[SensorEntity] = [
3939
EnphaseSiteLastUpdateSensor(coord),
4040
EnphaseCloudLatencySensor(coord),
4141
EnphaseSiteLastErrorCodeSensor(coord),
4242
EnphaseSiteBackoffEndsSensor(coord),
4343
]
44-
async_add_entities(site_entities, update_before_add=False)
45-
46-
site_energy_specs = {
44+
site_energy_specs: dict[str, tuple[str, str]] = {
4745
"solar_production": ("site_solar_production", "Site Solar Production"),
4846
"grid_import": ("site_grid_import", "Site Grid Import"),
4947
"grid_export": ("site_grid_export", "Site Grid Export"),
5048
"battery_charge": ("site_battery_charge", "Site Battery Charge"),
5149
"battery_discharge": ("site_battery_discharge", "Site Battery Discharge"),
5250
}
53-
known_site_flows: set[str] = set()
51+
site_entities.extend(
52+
EnphaseSiteEnergySensor(coord, flow_key, translation_key, name)
53+
for flow_key, (translation_key, name) in site_energy_specs.items()
54+
)
55+
async_add_entities(site_entities, update_before_add=False)
5456
known_serials: set[str] = set()
5557

5658
@callback
@@ -76,31 +78,9 @@ def _async_sync_chargers() -> None:
7678
async_add_entities(per_serial_entities, update_before_add=False)
7779
known_serials.update(serials)
7880

79-
@callback
80-
def _async_sync_site_energy() -> None:
81-
flows = getattr(coord, "site_energy", {}) or {}
82-
new_entities = []
83-
for flow_key, (translation_key, name) in site_energy_specs.items():
84-
if flow_key in known_site_flows:
85-
continue
86-
flow_data = flows.get(flow_key)
87-
if flow_data is None:
88-
continue
89-
new_entities.append(
90-
EnphaseSiteEnergySensor(coord, flow_key, translation_key, name)
91-
)
92-
known_site_flows.add(flow_key)
93-
if new_entities:
94-
async_add_entities(new_entities, update_before_add=False)
95-
96-
@callback
97-
def _async_sync_all() -> None:
98-
_async_sync_chargers()
99-
_async_sync_site_energy()
100-
101-
unsubscribe = coord.async_add_listener(_async_sync_all)
81+
unsubscribe = coord.async_add_listener(_async_sync_chargers)
10282
entry.async_on_unload(unsubscribe)
103-
_async_sync_all()
83+
_async_sync_chargers()
10484

10585

10686
class _BaseEVSensor(EnphaseBaseEntity, SensorEntity):

tests/components/enphase_ev/test_config_flow_coverage.py

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from unittest.mock import AsyncMock, MagicMock, patch
44

55
import pytest
6+
from voluptuous.schema_builder import Optional as VolOptional
67
from homeassistant import config_entries
78
from homeassistant.data_entry_flow import FlowResultType, AbortFlow
89
from pytest_homeassistant_custom_component.common import MockConfigEntry
@@ -184,12 +185,32 @@ async def test_devices_step_requires_site_only_opt_in(hass) -> None:
184185
"custom_components.enphase_ev.config_flow.async_fetch_chargers",
185186
AsyncMock(return_value=[]),
186187
):
187-
result = await flow.async_step_devices({CONF_SERIALS: ""})
188+
result = await flow.async_step_devices({})
188189

189190
assert result["type"] is FlowResultType.FORM
190191
assert result["errors"] == {"base": "serials_or_site_only_required"}
191192

192193

194+
@pytest.mark.asyncio
195+
async def test_devices_step_site_only_schema_allows_empty_serials(hass) -> None:
196+
flow = _make_flow(hass)
197+
flow._auth_tokens = TOKENS
198+
flow._selected_site_id = "site-123"
199+
flow._sites = {"site-123": "Garage"}
200+
with patch(
201+
"custom_components.enphase_ev.config_flow.async_fetch_chargers",
202+
AsyncMock(return_value=[]),
203+
):
204+
result = await flow.async_step_devices()
205+
206+
assert result["type"] is FlowResultType.FORM
207+
schema_keys = list(result["data_schema"].schema.keys())
208+
assert any(
209+
isinstance(key, VolOptional) and key.schema == CONF_SERIALS
210+
for key in schema_keys
211+
)
212+
213+
193214
@pytest.mark.asyncio
194215
async def test_devices_step_allows_site_only_entry(hass) -> None:
195216
site = SiteInfo(site_id="site-123", name="Garage Site")
@@ -217,7 +238,7 @@ async def test_devices_step_allows_site_only_entry(hass) -> None:
217238
)
218239
result = await hass.config_entries.flow.async_configure(
219240
devices["flow_id"],
220-
{CONF_SERIALS: "", CONF_SITE_ONLY: True, CONF_SCAN_INTERVAL: 55},
241+
{CONF_SITE_ONLY: True, CONF_SCAN_INTERVAL: 55},
221242
)
222243

223244
assert result["type"] is FlowResultType.CREATE_ENTRY

tests/components/enphase_ev/test_coordinator_remaining_coverage.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -707,6 +707,14 @@ def test_iter_serials_falls_back_to_sorted(coordinator_factory):
707707
assert ordered[:2] == ["A", "B"]
708708

709709

710+
def test_iter_serials_empty_when_site_only(coordinator_factory):
711+
coord = coordinator_factory()
712+
coord.serials = {"A"}
713+
coord._serial_order = ["A"]
714+
coord.site_only = True
715+
assert coord.iter_serials() == []
716+
717+
710718
def test_coerce_amp_and_amp_limits(coordinator_factory):
711719
coord = coordinator_factory()
712720
assert coord._coerce_amp(" ") is None

0 commit comments

Comments
 (0)