Skip to content

Commit 26437bb

Browse files
HarvsGNoRi2909joostlek
authored
Adds ConfigFlow for London Underground (home-assistant#152050)
Co-authored-by: Norbert Rittel <[email protected]> Co-authored-by: Joost Lekkerkerker <[email protected]>
1 parent 56d953a commit 26437bb

File tree

13 files changed

+704
-37
lines changed

13 files changed

+704
-37
lines changed
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,36 @@
11
"""The london_underground component."""
2+
3+
from __future__ import annotations
4+
5+
from homeassistant.const import Platform
6+
from homeassistant.core import HomeAssistant
7+
from homeassistant.helpers.aiohttp_client import async_get_clientsession
8+
9+
from .const import DOMAIN as DOMAIN
10+
from .coordinator import LondonTubeCoordinator, LondonUndergroundConfigEntry, TubeData
11+
12+
PLATFORMS: list[Platform] = [Platform.SENSOR]
13+
14+
15+
async def async_setup_entry(
16+
hass: HomeAssistant, entry: LondonUndergroundConfigEntry
17+
) -> bool:
18+
"""Set up London Underground from a config entry."""
19+
20+
session = async_get_clientsession(hass)
21+
data = TubeData(session)
22+
coordinator = LondonTubeCoordinator(hass, data, config_entry=entry)
23+
await coordinator.async_config_entry_first_refresh()
24+
25+
entry.runtime_data = coordinator
26+
# Forward the setup to the sensor platform
27+
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
28+
29+
return True
30+
31+
32+
async def async_unload_entry(
33+
hass: HomeAssistant, entry: LondonUndergroundConfigEntry
34+
) -> bool:
35+
"""Unload a config entry."""
36+
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
"""Config flow for London Underground integration."""
2+
3+
from __future__ import annotations
4+
5+
import asyncio
6+
import logging
7+
from typing import Any
8+
9+
from london_tube_status import TubeData
10+
import voluptuous as vol
11+
12+
from homeassistant.config_entries import (
13+
ConfigEntry,
14+
ConfigFlow,
15+
ConfigFlowResult,
16+
OptionsFlowWithReload,
17+
)
18+
from homeassistant.core import callback
19+
from homeassistant.helpers import selector
20+
from homeassistant.helpers.aiohttp_client import async_get_clientsession
21+
from homeassistant.helpers.typing import ConfigType
22+
23+
from .const import CONF_LINE, DEFAULT_LINES, DOMAIN, TUBE_LINES
24+
25+
_LOGGER = logging.getLogger(__name__)
26+
27+
28+
class LondonUndergroundConfigFlow(ConfigFlow, domain=DOMAIN):
29+
"""Handle a config flow for London Underground."""
30+
31+
VERSION = 1
32+
MINOR_VERSION = 1
33+
34+
@staticmethod
35+
@callback
36+
def async_get_options_flow(
37+
_: ConfigEntry,
38+
) -> LondonUndergroundOptionsFlow:
39+
"""Get the options flow for this handler."""
40+
return LondonUndergroundOptionsFlow()
41+
42+
async def async_step_user(
43+
self, user_input: dict[str, Any] | None = None
44+
) -> ConfigFlowResult:
45+
"""Handle the initial step."""
46+
errors: dict[str, str] = {}
47+
48+
if user_input is not None:
49+
session = async_get_clientsession(self.hass)
50+
data = TubeData(session)
51+
try:
52+
async with asyncio.timeout(10):
53+
await data.update()
54+
except TimeoutError:
55+
errors["base"] = "timeout_connect"
56+
except Exception:
57+
_LOGGER.exception("Unexpected error")
58+
errors["base"] = "cannot_connect"
59+
else:
60+
return self.async_create_entry(
61+
title="London Underground",
62+
data={},
63+
options={CONF_LINE: user_input.get(CONF_LINE, DEFAULT_LINES)},
64+
)
65+
66+
return self.async_show_form(
67+
step_id="user",
68+
data_schema=vol.Schema(
69+
{
70+
vol.Optional(
71+
CONF_LINE,
72+
default=DEFAULT_LINES,
73+
): selector.SelectSelector(
74+
selector.SelectSelectorConfig(
75+
options=TUBE_LINES,
76+
multiple=True,
77+
mode=selector.SelectSelectorMode.DROPDOWN,
78+
)
79+
),
80+
}
81+
),
82+
errors=errors,
83+
)
84+
85+
async def async_step_import(self, import_data: ConfigType) -> ConfigFlowResult:
86+
"""Handle import from configuration.yaml."""
87+
session = async_get_clientsession(self.hass)
88+
data = TubeData(session)
89+
try:
90+
async with asyncio.timeout(10):
91+
await data.update()
92+
except Exception:
93+
_LOGGER.exception(
94+
"Unexpected error trying to connect before importing config, aborting import "
95+
)
96+
return self.async_abort(reason="cannot_connect")
97+
98+
_LOGGER.warning(
99+
"Importing London Underground config from configuration.yaml: %s",
100+
import_data,
101+
)
102+
# Extract lines from the sensor platform config
103+
lines = import_data.get(CONF_LINE, DEFAULT_LINES)
104+
if "London Overground" in lines:
105+
_LOGGER.warning(
106+
"London Overground was removed from the configuration as the line has been divided and renamed"
107+
)
108+
lines.remove("London Overground")
109+
return self.async_create_entry(
110+
title="London Underground",
111+
data={},
112+
options={CONF_LINE: import_data.get(CONF_LINE, DEFAULT_LINES)},
113+
)
114+
115+
116+
class LondonUndergroundOptionsFlow(OptionsFlowWithReload):
117+
"""Handle options."""
118+
119+
async def async_step_init(
120+
self, user_input: dict[str, Any] | None = None
121+
) -> ConfigFlowResult:
122+
"""Manage the options."""
123+
if user_input is not None:
124+
_LOGGER.debug(
125+
"Updating london underground with options flow user_input: %s",
126+
user_input,
127+
)
128+
return self.async_create_entry(
129+
title="",
130+
data={CONF_LINE: user_input[CONF_LINE]},
131+
)
132+
133+
return self.async_show_form(
134+
step_id="init",
135+
data_schema=vol.Schema(
136+
{
137+
vol.Optional(
138+
CONF_LINE,
139+
default=self.config_entry.options.get(
140+
CONF_LINE,
141+
self.config_entry.data.get(CONF_LINE, DEFAULT_LINES),
142+
),
143+
): selector.SelectSelector(
144+
selector.SelectSelectorConfig(
145+
options=TUBE_LINES,
146+
multiple=True,
147+
mode=selector.SelectSelectorMode.DROPDOWN,
148+
)
149+
),
150+
}
151+
),
152+
)

homeassistant/components/london_underground/const.py

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66

77
CONF_LINE = "line"
88

9-
109
SCAN_INTERVAL = timedelta(seconds=30)
1110

1211
TUBE_LINES = [
@@ -18,7 +17,7 @@
1817
"Elizabeth line",
1918
"Hammersmith & City",
2019
"Jubilee",
21-
"London Overground",
20+
"London Overground", # no longer supported
2221
"Metropolitan",
2322
"Northern",
2423
"Piccadilly",
@@ -31,3 +30,20 @@
3130
"Weaver",
3231
"Windrush",
3332
]
33+
34+
# Default lines to monitor if none selected
35+
DEFAULT_LINES = [
36+
"Bakerloo",
37+
"Central",
38+
"Circle",
39+
"District",
40+
"DLR",
41+
"Elizabeth line",
42+
"Hammersmith & City",
43+
"Jubilee",
44+
"Metropolitan",
45+
"Northern",
46+
"Piccadilly",
47+
"Victoria",
48+
"Waterloo & City",
49+
]

homeassistant/components/london_underground/coordinator.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,23 +8,31 @@
88

99
from london_tube_status import TubeData
1010

11+
from homeassistant.config_entries import ConfigEntry
1112
from homeassistant.core import HomeAssistant
1213
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
1314

1415
from .const import DOMAIN, SCAN_INTERVAL
1516

1617
_LOGGER = logging.getLogger(__name__)
1718

19+
type LondonUndergroundConfigEntry = ConfigEntry[LondonTubeCoordinator]
20+
1821

1922
class LondonTubeCoordinator(DataUpdateCoordinator[dict[str, dict[str, str]]]):
2023
"""London Underground sensor coordinator."""
2124

22-
def __init__(self, hass: HomeAssistant, data: TubeData) -> None:
25+
def __init__(
26+
self,
27+
hass: HomeAssistant,
28+
data: TubeData,
29+
config_entry: LondonUndergroundConfigEntry,
30+
) -> None:
2331
"""Initialize coordinator."""
2432
super().__init__(
2533
hass,
2634
_LOGGER,
27-
config_entry=None,
35+
config_entry=config_entry,
2836
name=DOMAIN,
2937
update_interval=SCAN_INTERVAL,
3038
)

homeassistant/components/london_underground/manifest.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,12 @@
22
"domain": "london_underground",
33
"name": "London Underground",
44
"codeowners": ["@jpbede"],
5+
"config_flow": true,
56
"documentation": "https://www.home-assistant.io/integrations/london_underground",
7+
"integration_type": "service",
68
"iot_class": "cloud_polling",
79
"loggers": ["london_tube_status"],
810
"quality_scale": "legacy",
9-
"requirements": ["london-tube-status==0.5"]
11+
"requirements": ["london-tube-status==0.5"],
12+
"single_config_entry": true
1013
}

homeassistant/components/london_underground/sensor.py

Lines changed: 65 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -5,23 +5,26 @@
55
import logging
66
from typing import Any
77

8-
from london_tube_status import TubeData
98
import voluptuous as vol
109

1110
from homeassistant.components.sensor import (
1211
PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA,
1312
SensorEntity,
1413
)
15-
from homeassistant.core import HomeAssistant
16-
from homeassistant.exceptions import PlatformNotReady
17-
from homeassistant.helpers import config_validation as cv
18-
from homeassistant.helpers.aiohttp_client import async_get_clientsession
19-
from homeassistant.helpers.entity_platform import AddEntitiesCallback
14+
from homeassistant.config_entries import SOURCE_IMPORT
15+
from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant
16+
from homeassistant.data_entry_flow import FlowResultType
17+
from homeassistant.helpers import config_validation as cv, issue_registry as ir
18+
from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
19+
from homeassistant.helpers.entity_platform import (
20+
AddConfigEntryEntitiesCallback,
21+
AddEntitiesCallback,
22+
)
2023
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
2124
from homeassistant.helpers.update_coordinator import CoordinatorEntity
2225

23-
from .const import CONF_LINE, TUBE_LINES
24-
from .coordinator import LondonTubeCoordinator
26+
from .const import CONF_LINE, DOMAIN, TUBE_LINES
27+
from .coordinator import LondonTubeCoordinator, LondonUndergroundConfigEntry
2528

2629
_LOGGER = logging.getLogger(__name__)
2730

@@ -38,18 +41,54 @@ async def async_setup_platform(
3841
) -> None:
3942
"""Set up the Tube sensor."""
4043

41-
session = async_get_clientsession(hass)
42-
43-
data = TubeData(session)
44-
coordinator = LondonTubeCoordinator(hass, data)
44+
# If configuration.yaml config exists, trigger the import flow.
45+
# If the config entry already exists, this will not be triggered as only one config is allowed.
46+
result = await hass.config_entries.flow.async_init(
47+
DOMAIN, context={"source": SOURCE_IMPORT}, data=config
48+
)
49+
if (
50+
result.get("type") is FlowResultType.ABORT
51+
and result.get("reason") != "already_configured"
52+
):
53+
ir.async_create_issue(
54+
hass,
55+
DOMAIN,
56+
f"deprecated_yaml_import_issue_{result.get('reason')}",
57+
is_fixable=False,
58+
issue_domain=DOMAIN,
59+
severity=ir.IssueSeverity.WARNING,
60+
translation_key="deprecated_yaml_import_issue",
61+
translation_placeholders={
62+
"domain": DOMAIN,
63+
"integration_title": "London Underground",
64+
},
65+
)
66+
return
67+
68+
ir.async_create_issue(
69+
hass,
70+
HOMEASSISTANT_DOMAIN,
71+
"deprecated_yaml",
72+
is_fixable=False,
73+
issue_domain=DOMAIN,
74+
severity=ir.IssueSeverity.WARNING,
75+
translation_key="deprecated_yaml",
76+
translation_placeholders={
77+
"domain": DOMAIN,
78+
"integration_title": "London Underground",
79+
},
80+
)
4581

46-
await coordinator.async_refresh()
4782

48-
if not coordinator.last_update_success:
49-
raise PlatformNotReady
83+
async def async_setup_entry(
84+
hass: HomeAssistant,
85+
entry: LondonUndergroundConfigEntry,
86+
async_add_entities: AddConfigEntryEntitiesCallback,
87+
) -> None:
88+
"""Set up the London Underground sensor from config entry."""
5089

5190
async_add_entities(
52-
LondonTubeSensor(coordinator, line) for line in config[CONF_LINE]
91+
LondonTubeSensor(entry.runtime_data, line) for line in entry.options[CONF_LINE]
5392
)
5493

5594

@@ -58,11 +97,21 @@ class LondonTubeSensor(CoordinatorEntity[LondonTubeCoordinator], SensorEntity):
5897

5998
_attr_attribution = "Powered by TfL Open Data"
6099
_attr_icon = "mdi:subway"
100+
_attr_has_entity_name = True # Use modern entity naming
61101

62102
def __init__(self, coordinator: LondonTubeCoordinator, name: str) -> None:
63103
"""Initialize the London Underground sensor."""
64104
super().__init__(coordinator)
65105
self._name = name
106+
# Add unique_id for proper entity registry
107+
self._attr_unique_id = f"tube_{name.lower().replace(' ', '_')}"
108+
self._attr_device_info = DeviceInfo(
109+
identifiers={(DOMAIN, DOMAIN)},
110+
name="London Underground",
111+
manufacturer="Transport for London",
112+
model="Tube Status",
113+
entry_type=DeviceEntryType.SERVICE,
114+
)
66115

67116
@property
68117
def name(self) -> str:

0 commit comments

Comments
 (0)