Fix Journey location search: use EFA's own stopfinder, not Open-Meteo
Open-Meteo's geocoder is built for city-level weather lookups -- it knows
"Münster" but has no idea what "Münster Hauptbahnhof" means, so any
specific station name failed to resolve. Reused it from Weather without
checking it actually fit this use case.
The db_info reference project sidesteps this entirely by requiring the
user to pick existing Home Assistant person/zone entities, which already
carry precise coordinates from HA's own location system -- no equivalent
here, so this needed real geocoding.
Switched geocode() to query the same EFA instances' XML_STOPFINDER_REQUEST
endpoint instead -- the same system that answers the trip request, so
location resolution is guaranteed consistent with what trip planning
understands. Verified live: correctly resolves specific stations ("Münster
Hauptbahnhof" -> "Münster (Westf), Hbf", even the "Muenster" ue-spelling
the user actually typed), plain city names (falls back to the main
station), and district/neighborhood names (e.g. "Hiltrup" -> the actual
Hiltrup stop, not just the vague suburb entry) -- all via matchQuality
scoring and the API's own isBest flag. Re-verified the full pipeline
end-to-end through the real Flask route afterward.
This commit is contained in:
1 parent
c182fb2f19
commit
d5757a00df
1 file changed
+49
-18
+49
-18
@@ -26,20 +26,25 @@ from screens.render_utils import load_font, text_width, truncate_to_width
|
|||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
HTTP_TIMEOUT = 10
|
HTTP_TIMEOUT = 10
|
||||||
GEOCODE_URL = "https://geocoding-api.open-meteo.com/v1/search"
|
|
||||||
|
|
||||||
|
# Each EFA instance exposes both endpoints under the same base path --
|
||||||
|
# XML_TRIP_REQUEST2 for routing, XML_STOPFINDER_REQUEST for resolving a
|
||||||
|
# place name to a stop + coordinates. Using the stopfinder from the same
|
||||||
|
# system that will answer the trip request (rather than a generic
|
||||||
|
# city-level geocoder) is what makes searching for e.g. "Münster
|
||||||
|
# Hauptbahnhof" -- a specific station, not just a city -- work at all.
|
||||||
EFA_SOURCES = [
|
EFA_SOURCES = [
|
||||||
{
|
{
|
||||||
"name": "vrr",
|
"name": "vrr",
|
||||||
"url": "https://www.vrr.de/vrr-efa/XML_TRIP_REQUEST2",
|
"base": "https://www.vrr.de/vrr-efa/",
|
||||||
# vrr.de hangs until timeout for trips outside its home region
|
# vrr.de hangs until timeout for trips outside its home region
|
||||||
# instead of returning a fast error, so it's only queried when both
|
# instead of returning a fast error, so it's only queried when both
|
||||||
# ends of the trip roughly fall within North Rhine-Westphalia.
|
# ends of the trip roughly fall within North Rhine-Westphalia.
|
||||||
"bounds": {"lat_min": 50.3, "lat_max": 52.6, "lon_min": 5.7, "lon_max": 9.5},
|
"bounds": {"lat_min": 50.3, "lat_max": 52.6, "lon_min": 5.7, "lon_max": 9.5},
|
||||||
},
|
},
|
||||||
{"name": "bahnland-bayern", "url": "https://bahnland-bayern.de/efa/XML_TRIP_REQUEST2", "bounds": None},
|
{"name": "bahnland-bayern", "base": "https://bahnland-bayern.de/efa/", "bounds": None},
|
||||||
{"name": "efa", "url": "https://www.efa.de/hit-efa/XML_TRIP_REQUEST2", "bounds": None},
|
{"name": "efa", "base": "https://www.efa.de/hit-efa/", "bounds": None},
|
||||||
{"name": "efa-bw", "url": "http://www.efa-bw.de/nvbw/XML_TRIP_REQUEST2", "bounds": None},
|
{"name": "efa-bw", "base": "http://www.efa-bw.de/nvbw/", "bounds": None},
|
||||||
]
|
]
|
||||||
|
|
||||||
# Some EFA instances reject the default requests/python User-Agent as bot
|
# Some EFA instances reject the default requests/python User-Agent as bot
|
||||||
@@ -91,22 +96,48 @@ class JourneyScreen(Screen):
|
|||||||
return max(1, minutes) * 60
|
return max(1, minutes) * 60
|
||||||
|
|
||||||
def geocode(self, query: str):
|
def geocode(self, query: str):
|
||||||
"""Resolve a place name to coordinates -- same free Open-Meteo
|
"""Resolve a place/station name to coordinates via the same EFA
|
||||||
geocoding WeatherScreen uses, no key needed."""
|
instances used for trip requests (XML_STOPFINDER_REQUEST) -- not a
|
||||||
|
generic city-level geocoder, which has no idea what "Hauptbahnhof"
|
||||||
|
means. Tries each source in order (no bounds pre-filter here, since
|
||||||
|
we don't have coordinates yet to check bounds against); returns the
|
||||||
|
first usable match."""
|
||||||
|
params = {
|
||||||
|
"outputFormat": "rapidJSON",
|
||||||
|
"language": "de",
|
||||||
|
"type_sf": "any",
|
||||||
|
"name_sf": query,
|
||||||
|
"anyMaxSizeHitList": 5,
|
||||||
|
"coordOutputFormat": "WGS84[dd.ddddd]",
|
||||||
|
}
|
||||||
|
|
||||||
|
for source in EFA_SOURCES:
|
||||||
|
try:
|
||||||
response = requests.get(
|
response = requests.get(
|
||||||
GEOCODE_URL, params={"name": query, "count": 1, "language": "de"}, timeout=HTTP_TIMEOUT
|
source["base"] + "XML_STOPFINDER_REQUEST",
|
||||||
|
params=params, headers=REQUEST_HEADERS, timeout=HTTP_TIMEOUT,
|
||||||
)
|
)
|
||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
results = response.json().get("results") or []
|
data = response.json()
|
||||||
if not results:
|
except (requests.exceptions.RequestException, ValueError) as exc:
|
||||||
|
logger.warning("Stopfinder source '%s' failed, trying next: %s", source["name"], exc)
|
||||||
|
continue
|
||||||
|
|
||||||
|
match = self._best_location(data)
|
||||||
|
if match:
|
||||||
|
return match
|
||||||
|
|
||||||
return None
|
return None
|
||||||
top = results[0]
|
|
||||||
label_parts = [top.get("name"), top.get("admin1"), top.get("country")]
|
def _best_location(self, data: dict):
|
||||||
return {
|
locations = data.get("locations") or []
|
||||||
"latitude": top["latitude"],
|
if not locations:
|
||||||
"longitude": top["longitude"],
|
return None
|
||||||
"label": ", ".join(p for p in label_parts if p),
|
best = next((loc for loc in locations if loc.get("isBest")), locations[0])
|
||||||
}
|
coord = best.get("coord")
|
||||||
|
if not coord or len(coord) < 2:
|
||||||
|
return None
|
||||||
|
return {"latitude": coord[0], "longitude": coord[1], "label": best.get("name") or ""}
|
||||||
|
|
||||||
def check(self, settings: dict):
|
def check(self, settings: dict):
|
||||||
o_lat, o_lon = settings.get("origin_lat"), settings.get("origin_lon")
|
o_lat, o_lon = settings.get("origin_lat"), settings.get("origin_lon")
|
||||||
@@ -126,7 +157,7 @@ class JourneyScreen(Screen):
|
|||||||
continue
|
continue
|
||||||
try:
|
try:
|
||||||
response = requests.get(
|
response = requests.get(
|
||||||
source["url"], params=params, headers=REQUEST_HEADERS, timeout=HTTP_TIMEOUT
|
source["base"] + "XML_TRIP_REQUEST2", params=params, headers=REQUEST_HEADERS, timeout=HTTP_TIMEOUT
|
||||||
)
|
)
|
||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
data = response.json()
|
data = response.json()
|
||||||
|
|||||||
Reference in new issue
Block a user