Environment Canada station selector (#154307)

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
This commit is contained in:
Michael Davie
2025-10-12 16:34:16 -04:00
committed by GitHub
parent 3d130a9bdf
commit 4ca1ae61aa
3 changed files with 89 additions and 14 deletions

View File

@@ -6,11 +6,18 @@ import xml.etree.ElementTree as ET
import aiohttp
from env_canada import ECWeather, ec_exc
from env_canada.ec_weather import get_ec_sites_list
import voluptuous as vol
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
from homeassistant.const import CONF_LANGUAGE, CONF_LATITUDE, CONF_LONGITUDE
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.selector import (
SelectOptionDict,
SelectSelector,
SelectSelectorConfig,
SelectSelectorMode,
)
from .const import CONF_STATION, CONF_TITLE, DOMAIN
@@ -25,14 +32,16 @@ async def validate_input(data):
lang = data.get(CONF_LANGUAGE).lower()
if station:
# When station is provided, use it and get the coordinates from ECWeather
weather_data = ECWeather(station_id=station, language=lang)
else:
weather_data = ECWeather(coordinates=(lat, lon), language=lang)
await weather_data.update()
if lat is None or lon is None:
await weather_data.update()
# Always use the station's coordinates, not the user-provided ones
lat = weather_data.lat
lon = weather_data.lon
else:
# When no station is provided, use coordinates to find nearest station
weather_data = ECWeather(coordinates=(lat, lon), language=lang)
await weather_data.update()
return {
CONF_TITLE: weather_data.metadata.location,
@@ -46,6 +55,13 @@ class EnvironmentCanadaConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle a config flow for Environment Canada weather."""
VERSION = 1
_station_codes: list[dict[str, str]] | None = None
async def _get_station_codes(self) -> list[dict[str, str]]:
"""Get station codes, cached after first call."""
if self._station_codes is None:
self._station_codes = await get_ec_sites_list()
return self._station_codes
async def async_step_user(
self, user_input: dict[str, Any] | None = None
@@ -80,9 +96,21 @@ class EnvironmentCanadaConfigFlow(ConfigFlow, domain=DOMAIN):
self._abort_if_unique_id_configured()
return self.async_create_entry(title=info[CONF_TITLE], data=user_input)
station_codes = await self._get_station_codes()
data_schema = vol.Schema(
{
vol.Optional(CONF_STATION): str,
vol.Optional(CONF_STATION): SelectSelector(
SelectSelectorConfig(
options=[
SelectOptionDict(
value=station["value"], label=station["label"]
)
for station in station_codes
],
mode=SelectSelectorMode.DROPDOWN,
)
),
vol.Optional(
CONF_LATITUDE, default=self.hass.config.latitude
): cv.latitude,

View File

@@ -2,12 +2,12 @@
"config": {
"step": {
"user": {
"title": "Environment Canada: Location and language",
"description": "You can specify a location using either a station code or latitude/longitude coordinates.\n\nDefault behavior: If no station code is entered, the system uses the latitude/longitude values configured in your Home Assistant installation.\n\nStation code format: Station codes follow the format \"s0000123\" or simply \"123\".\n\nFind station codes at https://dd.weather.gc.ca/today/citypage_weather/docs/site_list_towns_en.csv.",
"title": "Environment Canada: weather location and language",
"description": "Select a weather station from the dropdown, or specify coordinates to use the closest station. The default coordinates are from your Home Assistant installation. Weather information can be retrieved in English or French.",
"data": {
"latitude": "[%key:common::config_flow::data::latitude%]",
"longitude": "[%key:common::config_flow::data::longitude%]",
"station": "Station code",
"station": "Weather station",
"language": "Weather information language"
}
}

View File

@@ -15,12 +15,17 @@ from homeassistant.data_entry_flow import FlowResultType
from tests.common import MockConfigEntry
FAKE_CONFIG = {
CONF_STATION: "ON/s1234567",
CONF_STATION: "123",
CONF_LANGUAGE: "English",
CONF_LATITUDE: 42.42,
CONF_LONGITUDE: -42.42,
}
FAKE_TITLE = "Universal title!"
FAKE_STATIONS = [
{"label": "Toronto, ON", "value": "123"},
{"label": "Ottawa, ON", "value": "456"},
{"label": "Montreal, QC", "value": "789"},
]
def mocked_ec():
@@ -40,10 +45,19 @@ def mocked_ec():
)
def mocked_stations():
"""Mock the station list."""
return patch(
"homeassistant.components.environment_canada.config_flow.get_ec_sites_list",
return_value=FAKE_STATIONS,
)
async def test_create_entry(hass: HomeAssistant) -> None:
"""Test creating an entry."""
with (
mocked_ec(),
mocked_stations(),
patch(
"homeassistant.components.environment_canada.async_setup_entry",
return_value=True,
@@ -66,12 +80,13 @@ async def test_create_same_entry_twice(hass: HomeAssistant) -> None:
entry = MockConfigEntry(
domain=DOMAIN,
data=FAKE_CONFIG,
unique_id="ON/s1234567-english",
unique_id="123-english",
)
entry.add_to_hass(hass)
with (
mocked_ec(),
mocked_stations(),
patch(
"homeassistant.components.environment_canada.async_setup_entry",
return_value=True,
@@ -101,9 +116,12 @@ async def test_create_same_entry_twice(hass: HomeAssistant) -> None:
async def test_exception_handling(hass: HomeAssistant, error) -> None:
"""Test exception handling."""
exc, base_error = error
with patch(
"homeassistant.components.environment_canada.config_flow.ECWeather",
side_effect=exc,
with (
mocked_stations(),
patch(
"homeassistant.components.environment_canada.config_flow.ECWeather",
side_effect=exc,
),
):
flow = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
@@ -121,6 +139,7 @@ async def test_lat_lon_not_specified(hass: HomeAssistant) -> None:
"""Test that the import step works when coordinates are not specified."""
with (
mocked_ec(),
mocked_stations(),
patch(
"homeassistant.components.environment_canada.async_setup_entry",
return_value=True,
@@ -136,3 +155,31 @@ async def test_lat_lon_not_specified(hass: HomeAssistant) -> None:
assert result["type"] is FlowResultType.CREATE_ENTRY
assert result["data"] == FAKE_CONFIG
assert result["title"] == FAKE_TITLE
async def test_coordinates_without_station(hass: HomeAssistant) -> None:
"""Test setup with coordinates but no station ID."""
with (
mocked_ec(),
mocked_stations(),
patch(
"homeassistant.components.environment_canada.async_setup_entry",
return_value=True,
),
):
# Config with coordinates but no station
config_no_station = {
CONF_LANGUAGE: "English",
CONF_LATITUDE: 42.42,
CONF_LONGITUDE: -42.42,
}
flow = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
result = await hass.config_entries.flow.async_configure(
flow["flow_id"], config_no_station
)
await hass.async_block_till_done()
assert result["type"] is FlowResultType.CREATE_ENTRY
assert result["data"] == FAKE_CONFIG
assert result["title"] == FAKE_TITLE