Skip to content

Commit edc2225

Browse files
committed
fix(realtime): station boards showing platform and track together (SZT-Q9)
Ústí n.L. hl.n. and Teplice v Čechách head the last column `Nást./kolej` and show `<platform> / <track>` (`1a / 9`, `3 / -`, `- / -`). The decoder knew only `Nástupiště` and `Kolej` and skipped such boards whole: they were read about 20 times a day each and nothing was recorded. Each assigned part now becomes its own assignment with its label. Ústí fixture: 14 assignments from 11 rows with a value.
1 parent 1232f99 commit edc2225

5 files changed

Lines changed: 1681 additions & 25 deletions

File tree

‎PROGRESS.md‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -244,6 +244,9 @@ and static acceptance (MobilityData GTFS validator, MOTIS import) is still open.
244244

245245
## Recent log
246246

247+
- **2026-10-11** — SZT-Q9: boards headed `Nást./kolej` (Ústí n.L. hl.n., Teplice v Čechách)
248+
show `<platform> / <track>` and were skipped whole by the decoder (read, never recorded);
249+
now split into a platform and a track assignment. Ústí fixture: 14 assignments from 11 rows.
247250
- **2026-10-10** — DUK-Q1 corrected: Teplice buses and RJÚK trains apply the local offset
248251
twice to their GPS time (`LastActivityDT` is right); the old "UTC digits" reading put them
249252
4 h ahead, so all their positions were dropped. Now detected per entry against the unit's

‎docs/sources/sz-tabule.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ SŽ map and are not used.
7474
| SZT-Q6 | a fixed number of rows (about 20 per table), not a fixed time span | demand scheduling reads busy stations more often (`[boards]` policy) |
7575
| SZT-Q7 | the `onlyForVlak` per-train variant is mostly empty | not used |
7676
| SZT-Q8 | addresses running services are reportedly IP-banned | proxy pool, no User-Agent, a 403 opens the circuit, a budget ceiling |
77+
| SZT-Q9 | a third header, `Nást./kolej` (Ústí n.L. hl.n., Teplice v Čechách), shows both: `<platform> / <track>`, each `-` until assigned (`3 / -`, `1a / 9`, `- / -`). The decoder knew only `Nástupiště` and `Kolej` and skipped these boards whole: read about 20 times a day each, nothing recorded until 2026-10-11 | split into a platform and a track assignment, each with its label; an unassigned part is left out |
7778

7879
## Demand (measured)
7980

‎src/obehy/realtime/sources/sz_tabule.py‎

Lines changed: 39 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,10 @@
44
becomes a vehicle-less observation, keyed by train number, carrying a `PlatformAssignment` for
55
the station (the request's SR70, kept in the archive's index line). Quirks normalized here:
66
SZT-Q1 (the 6-digit request code loses its check digit), SZT-Q2 (the last column's header says
7-
whether values are platforms or tracks), SZT-Q4 (`-` and `ND` are no assignment), SZT-Q5 (bare
8-
`HH:mm` scheduled times, resolved near reception). Every value is shown twice (desktop and
9-
mobile cells); only desktop cells are read, by their column class.
7+
whether values are platforms or tracks), SZT-Q9 (or both: `Nást./kolej`, `1a / 9`), SZT-Q4 (`-`
8+
and `ND` are no assignment), SZT-Q5 (bare `HH:mm` scheduled times, resolved near reception).
9+
Every value is shown twice (desktop and mobile cells); only desktop cells are read, by their
10+
column class.
1011
"""
1112

1213
from __future__ import annotations
@@ -30,10 +31,17 @@
3031

3132
SOURCE = "sz-tabule"
3233
CHANNEL = "board"
33-
DECODER_VERSION = 1
34+
DECODER_VERSION = 2
3435

3536
TABLES: dict[str, Literal["arrival", "departure"]] = {"prijezdy": "arrival", "odjezdy": "departure"}
36-
LABELS: dict[str, PlatformLabel] = {"nástupiště": "platform", "kolej": "track"}
37+
# SZT-Q2/Q9: the last column's header names what its values are; `Nást./kolej` values are
38+
# `<platform> / <track>`.
39+
LABELS: dict[str, tuple[PlatformLabel, ...]] = {
40+
"nástupiště": ("platform",),
41+
"kolej": ("track",),
42+
"nást./kolej": ("platform", "track"),
43+
}
44+
PART_SEPARATOR = " / "
3745
UNASSIGNED = frozenset({"", "-", "ND"}) # SZT-Q4
3846
HHMM = re.compile(r"^(\d{1,2}):(\d{2})$")
3947
COLUMN = re.compile(r"^inTaCol[PO]-\d+$")
@@ -135,19 +143,18 @@ def decode(
135143
observations: list[Observation] = []
136144
item = 0
137145
for table in board.tables:
138-
label = _label(table)
146+
labels = _labels(table)
139147
scheduled_column = next(
140148
(column for column, name in table.header if name == "Pravidelný"), None
141149
)
142150
platform_column = table.header[-1][0] if table.header else None
143-
if label is None or scheduled_column is None or platform_column is None:
151+
if labels is None or scheduled_column is None or platform_column is None:
144152
continue
145153
for row in table.rows:
146-
fact = _row(
147-
row, table.kind, station, label, scheduled_column, platform_column, received_at
154+
facts = _row(
155+
row, table.kind, station, labels, scheduled_column, platform_column, received_at
148156
)
149-
if fact is not None:
150-
key, assignment = fact
157+
for key, assignment in facts:
151158
observations.append(
152159
Observation(
153160
source=SOURCE,
@@ -164,38 +171,45 @@ def decode(
164171
return observations
165172

166173

167-
def _label(table: _Table) -> PlatformLabel | None:
174+
def _labels(table: _Table) -> tuple[PlatformLabel, ...] | None:
168175
if not table.header:
169176
return None
170-
return LABELS.get(table.header[-1][1].strip().lower())
177+
return LABELS.get(" ".join(table.header[-1][1].split()).lower())
171178

172179

173180
def _row(
174181
row: list[_Cell],
175182
kind: Literal["arrival", "departure"],
176183
station: str,
177-
label: PlatformLabel,
184+
labels: tuple[PlatformLabel, ...],
178185
scheduled_column: str,
179186
platform_column: str,
180187
received_at: Instant,
181-
) -> tuple[Fact, PlatformAssignment] | None:
188+
) -> list[tuple[Fact, PlatformAssignment]]:
182189
desktop = [cell for cell in row if not cell.mobile]
183190
train = next((cell.train for cell in row if cell.train), None)
184191
scheduled = next((c.text for c in desktop if c.column == scheduled_column), None)
185192
value = next((c.text for c in desktop if c.column == platform_column), None)
186-
if train is None or not train.isdigit() or scheduled is None:
187-
return None
188-
if value is None or value in UNASSIGNED:
189-
return None
193+
if train is None or not train.isdigit() or scheduled is None or value is None:
194+
return []
195+
parts = value.split(PART_SEPARATOR) if len(labels) > 1 else [value]
196+
if len(parts) != len(labels):
197+
return []
198+
shown: list[tuple[PlatformLabel, str]] = [
199+
(label, part.strip())
200+
for label, part in zip(labels, parts, strict=True)
201+
if part.strip() not in UNASSIGNED
202+
]
203+
if not shown:
204+
return []
190205
match = HHMM.match(scheduled)
191206
if match is None:
192-
return None
207+
return []
193208
hours, minutes = int(match.group(1)), int(match.group(2))
194209
if hours > 23 or minutes > 59:
195-
return None
210+
return []
196211
when = resolve_clock(time(hours, minutes), received_at)
197212
if when is None:
198-
return None
199-
return TripKey("czptt:train_number", train), PlatformAssignment(
200-
station, kind, when, value, label
201-
)
213+
return []
214+
key = TripKey("czptt:train_number", train)
215+
return [(key, PlatformAssignment(station, kind, when, part, label)) for label, part in shown]

0 commit comments

Comments
 (0)