summaryrefslogtreecommitdiff
path: root/bin
diff options
context:
space:
mode:
Diffstat (limited to 'bin')
-rwxr-xr-xbin/efa-m778
1 files changed, 711 insertions, 67 deletions
diff --git a/bin/efa-m b/bin/efa-m
index 1447dac..2c768c2 100755
--- a/bin/efa-m
+++ b/bin/efa-m
@@ -2,66 +2,292 @@
use strict;
use warnings;
use 5.010;
+use utf8;
-our $VERSION = '1.01';
+our $VERSION = '3.15';
binmode( STDOUT, ':encoding(utf-8)' );
-use Getopt::Long qw(:config no_ignore_case);
-use List::Util qw(max);
-use Travel::Status::DE::VRR;
+use Encode qw(decode);
+use Getopt::Long qw(:config no_ignore_case bundling);
+use JSON;
+use List::Util qw(first max none);
+use Travel::Status::DE::EFA;
+
+my $service = 'VRR';
+my $efa_url;
+my $efa_encoding;
+my $use_cache = 1;
+my $cache;
+my ( $json_output, $raw_json_output );
+my ( $date, $time, $input_type, $list_lines, $offset, $relative_times );
+my ( $full_routes, $filter_via, $show_jid );
+my ( $timeout, $developer_mode );
+my ( @grep_lines, @grep_platforms, @grep_mots );
+my ( %edata, @edata_pre );
+my ( $list_services, $discover_and_print, $discover );
+my $efa;
+
+my %occupancy_map = (
+ MANY_SEATS => '.',
+ FEW_SEATS => 'o',
+ STANDING_ONLY => '*',
+ FULL => '!',
+ unknown => '?',
+);
-my ( $date, $time, $input_type, $list_lines );
-my ( @grep_lines, @grep_platforms );
+@ARGV = map { decode( 'UTF-8', $_ ) } @ARGV;
GetOptions(
- 'd|date=s' => \$date,
- 'h|help' => sub { show_help(0) },
- 'l|line=s@' => \@grep_lines,
- 'L|linelist' => \$list_lines,
- 'p|platform=s@' => \@grep_platforms,
- 't|time=s' => \$time,
- 'V|version' => \&show_version,
+ 'A|auto-url|discover-and-print' => \$discover_and_print,
+ 'd|date=s' => \$date,
+ 'D|discover' => \$discover,
+ 'h|help' => sub { show_help(0) },
+ 'j|with-jid' => \$show_jid,
+ 'l|line=s@' => \@grep_lines,
+ 'L|linelist' => \$list_lines,
+ 'list' => \$list_services,
+ 'm|mot=s@' => \@grep_mots,
+ 'o|offset=i' => \$offset,
+ 'O|output=s@' => \@edata_pre,
+ 'p|platform=s@' => \@grep_platforms,
+ 'r|relative' => \$relative_times,
+ 's|service=s' => \$service,
+ 't|time=s' => \$time,
+ 'timeout=i' => \$timeout,
+ 'u|efa-url=s' => \$efa_url,
+ 'v|via=s' => \$filter_via,
+ 'V|track-via=s' => \$filter_via,
+ 'cache!' => \$use_cache,
+ 'json' => \$json_output,
+ 'raw-json' => \$raw_json_output,
+ 'devmode' => \$developer_mode,
+ 'version' => \&show_version,
) or show_help(1);
-if ( @ARGV != 2 ) {
+if ($list_services) {
+ show_services();
+}
+
+if ( @ARGV < 1 or @ARGV > 2 ) {
show_help(1);
}
+if ($use_cache) {
+ my $cache_path = ( $ENV{XDG_CACHE_HOME} // "$ENV{HOME}/.cache" )
+ . '/Travel-Status-DE-EFA';
+ eval {
+ require Cache::File;
+ $cache = Cache::File->new(
+ cache_root => $cache_path,
+ default_expires => '90 seconds',
+ lock_level => Cache::File::LOCK_LOCAL(),
+ );
+ };
+ if ($@) {
+ $cache = undef;
+ }
+}
+
# --line=foo,bar support
+@edata_pre = split( qr{,}, join( q{,}, @edata_pre ) );
@grep_lines = split( qr{,}, join( q{,}, @grep_lines ) );
+@grep_mots = split( qr{,}, join( q{,}, @grep_mots ) );
@grep_platforms = split( qr{,}, join( q{,}, @grep_platforms ) );
-my ( $place, $input ) = @ARGV;
+my ( $place, $input, $coord, $stopseq, $stopfinder );
+
+if ( @ARGV == 1 ) {
+ if ( $ARGV[0]
+ =~ m{ ^ ([^@]*) @ ([^@]*) [(] ([^T]*) T ([^)]*) [)] (.*) $ }x )
+ {
+ $stopseq = {
+ stateless => $1,
+ stop_id => $2,
+ date => $3,
+ time => $4,
+ key => $5
+ };
+ }
+ elsif ( $ARGV[0] =~ m{ ^ [?] (?<name> .*) $ }x ) {
+ $stopfinder = {
+ name => $+{name},
+ };
+ }
+ elsif ( $ARGV[0] =~ m{ ^ (?<lat> [0-9.]+ ) : (?<lon> [0-9].+ ) $ }x ) {
+ $coord = {
+ lat => $+{lat},
+ lon => $+{lon},
+ };
+ }
+ else {
+ $input = $ARGV[0];
+ }
+}
+else {
+ ( $place, $input ) = @ARGV;
+}
-if ( $input =~ s{ ^ (?<type> address|poi|stop) : }{}x ) {
+if ( $input and $input =~ s{ ^ (?<type> address|poi|stop|stopID) : }{}x ) {
$input_type = $+{type};
}
-my $status = Travel::Status::DE::VRR->new(
- date => $date,
- place => $place,
- name => $input,
- time => $time,
- type => $input_type,
-);
+for my $efield (@edata_pre) {
+ if ( $efield eq 'a' ) { $edata{route_after} = 1; $full_routes = 1 }
+ elsif ( $efield eq 'b' ) { $edata{route_before} = 1; $full_routes = 1 }
+ elsif ( $efield eq 'f' ) { $edata{fullroute} = 1; $full_routes = 1 }
+ elsif ( $efield eq 'r' ) { $edata{route} = 1; $full_routes = 1 }
+ elsif ( $efield eq 'i' ) { $edata{info} = 1 }
+ elsif ( $efield eq 'm' ) { $edata{messages} = 1 }
+ else { $edata{$efield} = 1 }
+}
+if ($filter_via) {
+ $full_routes = 1;
+}
+
+if ($efa_url) {
+ $service = undef;
+}
+elsif ($service) {
+ my $service_ref = Travel::Status::DE::EFA::get_service($service);
+ if ( not $service_ref ) {
+ printf STDERR (
+ "Error: Unknown service '%s'. See 'efa-m --list' for a "
+ . "list of supported service names\n",
+ $service
+ );
+ exit 1;
+ }
+ $efa_encoding = $service_ref->{encoding};
+ $efa_url = undef;
+}
+
+sub new_efa {
+ my ( $s, $u ) = @_;
+ my $res = Travel::Status::DE::EFA->new(
+ service => $s,
+ efa_url => $u,
+ cache => $cache,
+ date => $date,
+ developer_mode => $developer_mode,
+ efa_encoding => $efa_encoding,
+ full_routes => $full_routes,
+ place => $place,
+ name => $input,
+ coord => $coord,
+ stopfinder => $stopfinder,
+ stopseq => $stopseq,
+ time => $time,
+ type => $input_type,
+ timeout => $timeout,
+ );
+
+ return $res;
+}
sub show_help {
my ($code) = @_;
- print "Usage: efa-m [-d <dd.mm.yyyy>] [-t <hh:mm>] <city> <station>\n"
+ print "Usage: efa-m [-d <dd.mm.yyyy>] [-t <hh:mm>] <stop>\n"
. "See also: man efa-m\n";
exit $code;
}
+sub show_services {
+ printf( "%-45s %-14s %s\n\n", 'service', 'abbr. (-s)', 'url (-u)' );
+ for my $shortname ( Travel::Status::DE::EFA::get_service_ids() ) {
+ my $service = Travel::Status::DE::EFA::get_service($shortname);
+ printf( "%-45s %-14s %s\n",
+ $service->{name}, $shortname, $service->{url} );
+ }
+
+ exit 0;
+}
+
sub show_version {
say "efa-m version ${VERSION}";
exit 0;
}
+sub format_delay {
+ my ( $delay, $len ) = @_;
+ if ( $delay and $len ) {
+ return sprintf( "(%+${len}d)", $delay );
+ }
+ return q{};
+}
+
+sub format_route {
+ my (@route) = @_;
+
+ my $output = q{};
+
+ for my $stop (@route) {
+ if ( not $stop ) {
+ say 'BUG';
+ next;
+ }
+ my $occupancy
+ = $stop->occupancy ? format_occupancy( $stop->occupancy ) : q{ };
+ my $delay = q{ };
+ if ( $stop->delay ) {
+ $delay = sprintf( '(%+3d)', $stop->delay );
+ }
+ if ( $stop->is_cancelled ) {
+ $output .= sprintf(
+ " --:-- %s %s %35s %s\n",
+ $delay, $occupancy, $stop->full_name, $stop->platform // q{},
+ );
+ }
+ elsif ( defined $stop->arr and defined $stop->dep ) {
+ if ( $stop->arr->epoch == $stop->dep->epoch ) {
+ $output .= sprintf(
+ " %5s %s %s %35s %s\n",
+ $stop->arr->strftime('%H:%M'),
+ $delay, $occupancy, $stop->full_name,
+ $stop->platform // q{},
+ );
+ }
+ else {
+ $output .= sprintf(
+ "%5s → %5s %s %s %35s %s\n",
+ $stop->arr->strftime('%H:%M'),
+ $stop->dep->strftime('%H:%M'),
+ $delay,
+ $occupancy,
+ $stop->full_name,
+ $stop->platform // q{},
+ );
+ }
+ }
+ elsif ( defined $stop->arr ) {
+ $output .= sprintf(
+ "%5s %s %s %35s %s\n",
+ $stop->arr->strftime('%H:%M'),
+ $delay, $occupancy, $stop->full_name, $stop->platform,
+ );
+ }
+ elsif ( defined $stop->dep ) {
+ $output .= sprintf(
+ " %5s %s %s %35s %s\n",
+ $stop->dep->strftime('%H:%M'),
+ $delay, $occupancy, $stop->full_name, $stop->platform,
+ );
+ }
+ elsif ( $stop->full_name ) {
+ $output .= sprintf( " %s %s %35s %s\n",
+ $delay, $occupancy, $stop->full_name, $stop->platform, );
+ }
+ else {
+ $output .= "?\n";
+ }
+ }
+ return $output;
+}
+
sub display_result {
my (@lines) = @_;
@@ -71,41 +297,125 @@ sub display_result {
die("Nothing to show\n");
}
- for my $i ( 0 .. 3 ) {
+ for my $i ( 0 .. 4 ) {
$line_length[$i] = max map { length( $_->[$i] ) } @lines;
}
for my $line (@lines) {
- if ( length( $line->[4] ) ) {
- $line->[4] =~ tr{\n\x0d}{ }s;
- chomp $line->[4];
+ if ( $edata{messages} and $line->[5]->hints ) {
print "\n";
- for my $info_line ( split( qr{\n}, $line->[4] ) ) {
- say "# ${info_line}";
+ for my $hint ( $line->[5]->hints ) {
+ $hint =~ tr{\n\x0d}{ }s;
+ chomp $hint;
+ say "# ${hint}";
}
}
printf(
join( q{ }, ( map { "%-${_}s" } @line_length ) ) . "\n",
- @{$line}[ 0 .. 3 ]
+ @{$line}[ 0 .. 4 ]
);
+
+ if ( $line->[6] ) {
+ say $line->[6];
+ }
}
return;
}
+sub show_coord {
+ my $max_len = max map { length( $_->full_name ) } $efa->results;
+ for my $stop ( $efa->results ) {
+ printf(
+ "%5.1f km %-${max_len}s %s\n",
+ $stop->distance_m * 1e-3,
+ $stop->full_name, $stop->id_code
+ );
+ }
+}
+
+sub show_stopfinder {
+ my $max_len = max map { length( $_->full_name ) } $efa->results;
+ for my $stop ( $efa->results ) {
+ printf( "%-${max_len}s %s %s\n",
+ $stop->full_name, $stop->id_num, $stop->id_code );
+ }
+}
+
+sub show_stopseq {
+ my $trip = $efa->result;
+
+ printf( "%s %s → %s\n", $trip->type, $trip->line // q{}, $trip->dest_name );
+
+ printf(
+ "Fahrt %s am %s\n",
+ $trip->number || $stopseq->{stateless},
+ ( $trip->route )[0]->sched_dep->strftime('%d.%m.%Y'),
+ );
+ say q{};
+
+ my $occupancy_len = 0;
+ my $delay_len = 0;
+ my $inner_delay_len = 0;
+ my $max_delay = max map { abs( $_->delay // 0 ) } $trip->route;
+ if ($max_delay) {
+ $inner_delay_len = length($max_delay) + 1;
+ $delay_len = length( sprintf( '(%+d)', $max_delay ) ) + 1;
+ }
+ if ( first { $_->occupancy } $trip->route ) {
+ $occupancy_len = 2;
+ }
+
+ if ( first { $_->is_cancelled } $trip->route and $delay_len < 3 ) {
+ $delay_len = 3;
+ }
+
+ for my $stop ( $trip->route ) {
+ printf(
+ "%s → %s%${delay_len}s %-${occupancy_len}s%s (%s) %s\n",
+ $stop->arr ? $stop->arr->strftime('%H:%M')
+ : q{ },
+ $stop->dep ? $stop->dep->strftime('%H:%M')
+ : q{ },
+ $stop->is_cancelled ? 'XX'
+ : (
+ $stop->delay
+ ? sprintf( " (%+${inner_delay_len}d)", $stop->delay )
+ : q{}
+ ),
+ $stop->occupancy ? format_occupancy( $stop->occupancy ) : q{},
+ $stop->full_name,
+ $stop->niveau // q{?},
+ $stop->platform // q{}
+ );
+ }
+}
+
sub show_lines {
my @output;
- for my $l ( $status->lines ) {
+ for my $l ( $efa->lines ) {
- if ( @grep_lines and not( $l->name ~~ \@grep_lines ) ) {
+ if ( ( @grep_lines and none { $l->number eq $_ } @grep_lines )
+ or ( @grep_mots and none { $l->mot_name eq $_ } @grep_mots ) )
+ {
next;
}
- push( @output,
- [ $l->type, $l->name, $l->direction // q{}, $l->route // q{} ] );
+ if ( @grep_mots and none { $l->mot_name eq $_ } @grep_mots ) {
+ next;
+ }
+
+ push(
+ @output,
+ [
+ $l->type, $l->number,
+ $l->direction // q{}, q{},
+ $l->route // q{}
+ ]
+ );
}
display_result(@output);
@@ -113,51 +423,195 @@ sub show_lines {
return;
}
+sub format_occupancy {
+ my ($occupancy) = @_;
+
+ return $occupancy_map{$occupancy} // $occupancy_map{unknown};
+}
+
sub show_results {
my @output;
- for my $d ( $status->results ) {
-
- my $platform = $d->platform;
- my $dtime = $d->time;
+ my $delay_len = 0;
+ my $delay_fmt = 0;
+ for my $d ( $efa->results ) {
+ if ( $d->delay ) {
+ $delay_len = max( $delay_len, length( $d->delay ) + 1 );
+ }
+ }
+ if ($delay_len) {
+ $delay_fmt = $delay_len + 3;
+ }
- if ( $d->platform_db ) {
- $platform .= ' (DB)';
+ if ( scalar $efa->stops > 1 ) {
+ for my $stop ( $efa->stops ) {
+ say $stop->full_name;
}
+ }
+ elsif ( $efa->stop->full_name ) {
+ say $efa->stop->full_name;
+ }
+
+ for my $d ( $efa->results ) {
+
+ my @output_line;
+ my $platform = $d->platform;
+ my $dtime = (
+ $relative_times
+ ? sprintf( '%2d min', $d->countdown )
+ : $d->datetime->strftime('%H:%M')
+ );
if (
- ( @grep_lines and not( $d->line ~~ \@grep_lines ) )
+ ( @grep_lines and none { $d->line eq $_ } @grep_lines )
+ or ( @grep_mots and none { $d->mot_name eq $_ } @grep_mots )
or ( @grep_platforms
- and not( $platform ~~ \@grep_platforms ) )
+ and none { $platform eq $_ } @grep_platforms )
+ or ( $offset and $d->countdown < $offset )
+ or ( $filter_via
+ and
+ not( first { $_->name =~ m{$filter_via}io } $d->route_post ) )
)
{
next;
}
- if ( $d->delay ) {
- if ($d->delay eq '-9999') {
- $dtime .= ' CANCELED';
+ if ( $d->is_cancelled ) {
+ if ($relative_times) {
+ next;
}
else {
- $dtime .= ' (+' . $d->delay . ')';
+ $dtime = '--:--';
}
}
+ elsif ($filter_via) {
+ my $via = first { $_->name =~ m{$filter_via}io } $d->route_post;
+ $dtime
+ .= ' → '
+ . $via->arr->clone->add( minutes => $d->delay // 0 )
+ ->strftime('%H:%M');
+ }
+ if ( $d->delay ) {
+ $dtime .= ' ' . format_delay( $d->delay, $delay_len );
+ }
+
+ my $line = $d->line;
+ if ( ( length($line) > 10 or not $line )
+ and $d->train_type
+ and $d->train_no )
+ {
+ $line = $d->train_type . ' ' . $d->train_no;
+ }
+
+ @output_line = ( $dtime, $platform, $line, q{}, $d->destination, $d );
- push( @output,
- [ $dtime, $platform, $d->line, $d->destination, $d->info ] );
+ if ($show_jid) {
+ $output_line[2] .= ' ' . $d->id;
+ }
+
+ if ( $edata{route} ) {
+ $output_line[3]
+ = join( q{ }, map { $_->name } $d->route_interesting );
+ }
+ elsif ( $d->occupancy ) {
+ $output_line[3] = format_occupancy( $d->occupancy );
+ }
+
+ if ( $edata{fullroute} ) {
+ $output_line[6]
+ = format_route( $d->route_pre )
+ . ' -' x 30 . "\n"
+ . format_route( $d->route_post );
+ }
+ elsif ( $edata{route_after} ) {
+ $output_line[6] = format_route( $d->route_post );
+ }
+ elsif ( $edata{route_before} ) {
+ $output_line[6] = format_route( reverse $d->route_pre );
+ }
+
+ push( @output, \@output_line );
}
display_result(@output);
+ if ( $edata{info} ) {
+ for my $info ( $efa->infos ) {
+ say q{};
+ if ( $info->subject and $info->subtitle ne $info->subject ) {
+ printf( "# %s\n%s\n", $info->subtitle, $info->subject );
+ }
+ else {
+ printf( "# %s\n", $info->subtitle );
+ }
+ }
+ }
+
return;
}
-if ( my $err = $status->errstr ) {
+if ( $discover or $discover_and_print ) {
+ for my $shortname ( Travel::Status::DE::EFA::get_service_ids() ) {
+ $efa = new_efa($shortname);
+ if ( $efa and not $efa->errstr ) {
+ if ($discover_and_print) {
+ last;
+ }
+ my $service_ref = Travel::Status::DE::EFA::get_service($shortname);
+ printf(
+ "%s / %s (%s)\n -> efa-m -s %s %s\n\n",
+ $service_ref->{name}, $shortname, $service_ref->{url},
+ $shortname, join( q{ }, map { "'$_'" } @ARGV ),
+ );
+ }
+ }
+ if ($discover) {
+ exit 0;
+ }
+}
+
+$efa = new_efa( $service, $efa_url );
+
+if ( my $err = $efa->errstr ) {
say STDERR "Request error: ${err}";
+
+ if ( $efa->place_candidates ) {
+ say 'You might want to try one of the following places:';
+ for my $candidate ( $efa->place_candidates ) {
+ printf( "%d %s\n", $candidate->id_num, $candidate->name );
+ }
+ }
+ elsif ( $efa->name_candidates ) {
+ say 'You might want to try one of the following names:';
+ for my $candidate ( $efa->name_candidates ) {
+ printf( "%d %s\n", $candidate->id_num, $candidate->name );
+ }
+ }
+
exit 2;
}
-if ($list_lines) {
+if ($json_output) {
+ if ($stopseq) {
+ say JSON->new->canonical->convert_blessed->encode( $efa->result );
+ }
+ else {
+ say JSON->new->canonical->convert_blessed->encode( [ $efa->results ] );
+ }
+}
+elsif ($raw_json_output) {
+ say JSON->new->canonical->convert_blessed->encode( $efa->{response} );
+}
+elsif ($coord) {
+ show_coord();
+}
+elsif ($stopfinder) {
+ show_stopfinder();
+}
+elsif ($stopseq) {
+ show_stopseq();
+}
+elsif ($list_lines) {
show_lines();
}
else {
@@ -168,47 +622,223 @@ __END__
=head1 NAME
-efa-m - Unofficial interface to the efa.vrr.de departure monitor
+efa-m - Unofficial interface to the efa.vrr.de departure and trip monitor
=head1 SYNOPSIS
-B<efa-m> [B<-d> I<date>] [B<-t> I<time>] I<city> [I<type>B<:>]I<name>
+B<efa-m> [B<-jLr>] [B<-d> I<dd.mm.yyyy>] [B<-t> I<hh:mm>]
+[B<-l> I<lines>] [B<-p> I<platforms>] [B<-s> I<service>]
+[I<city>] [I<type>B<:>]I<name>
+
+B<efa-m> [B<-s> I<service>] B<?>I<query>|I<lat>B<:>I<lon>
+
+B<efa-m> [B<-s> I<service>] I<tripid>
=head1 VERSION
-version 1.01
+version 3.15
=head1 DESCRIPTION
-B<efa-m> lists upcoming tram, bus and train departures at the location I<name>
-in I<city>.
+B<efa-m> is an interface to EFA public transport services.
+
+It can serve as a departure monitor, request details about a specific
+trip/journey, and look up public transport stops by name or geolocation.
+The operating mode depends on the contents of its mandatory argument.
+
+=head2 Departure Monitor (I<name>)
+
+Shows departures at I<name> or I<city> I<name>; I<name> may also be a stop ID
+number or code. For each departure, B<efa-m> shows
+
+=over
+
+=item * estimated departure time (including delay, if available),
+
+=item * delay in minutes,
+
+=item * platform,
+
+=item * line,
+
+=item * expected occupation (range . o * !, if available), and
+
+=item * destination.
+
+=back
+
+If I<city> is specified, I<name> refers to a location within I<city>.
+Otherwise, I<name> must be self-contained. I.e., both C<< efa-m Essen Hbf >>
+and C<< efa-m "Essen Hbf" >> are valid. Note, however, than C<< efa-m E Hbf >>
+works, but C<< efa-m "E Hbf" >> does not.
By default, I<name> refers to a stop, this can be changed by specifying
I<type>. Supported types are B<address> and B<poi> (point of interest).
+=head2 Location Search (B<?>I<query>|I<lat>B<:>I<lon>)
+
+List stops that match I<query> or that are located in the vicinity of
+I<lat>B<:>I<lon> geocoordinates. In addition to stop names, the output also
+includes stop ID codes (both modes) and numbers (only available in I<query>
+mode).
+
+=head2 Trip Details (I<JourneyID>)
+
+List trip information including arrival and departure time, name, and platform
+of each stop on the trip's route.
+
=head1 OPTIONS
+Values in brackets indicate options that only apply to the corresponding
+operating mode(s).
+
=over
-=item B<-d>, B<--date> I<dd.mm.yyyy>
+=item B<-A>, B<--auto-url>, B<--discover-and-print> (monitor)
+
+Probe all known EFA services for the specified stop. Print the first result
+which was not an error.
-Show departures for I<date> instead of today
+Note that this may take a while and will not necessarily return the best
+result. Also, using thi option by default is not recommended, as it puts EFA
+services under considerable additional load.
-=item B<-l>, B<--line> I<lines>
+=item B<-d>, B<--date> I<dd.mm.yyyy> (monitor)
+
+Show departures for I<date> instead of today.
+May also be specified as I<dd.mm.>
+
+=item B<-D>, B<--discover> (monitor)
+
+Probe all known EFA services for the specified stop. Print the URLs and names
+of all services which did not return an error.
+
+=item B<-j>, B<--with-jid> (monitor)
+
+Show journey ID for each departure.
+The ID can be used to query details with a subsequent B<efa-m> invocation
+(trip details mode).
+
+=item B<--json>
+
+Print result(s) as JSON and exit. This is a dump of internal data structures
+and not guaranteed to remain stable between minor versions. Please use the
+Travel::Status::DE::EFA(3pm) module if you need a proper API.
+
+=item B<-L>, B<--linelist> (monitor)
+
+Do not show departures. Instead, list all lines serving the specified place.
+Note that this information may be incomplete -- only lines which are in
+service either at the time of the B<efa-m> call or at the time specifed
+using B<--date> and B<--time> are guaranteed to be included.
+
+=item B<-l>, B<--line> I<lines> (monitor)
Only show departures of I<lines> (comma-separatad list, option may be
repeated)
-=item B<-p>, B<--platform> I<platforms>
+=item B<--list>
+
+List supported EFA services with their URLs (see B<-u>) and abbreviations (see
+B<-s>).
+
+=item B<-m>, B<--mot> I<motlist> (monitor)
+
+Only show departures whose type appears in I<motlist> (comma-separated list,
+this option may be repeated).
+
+The following departure types ("modes of transport") are supported:
+zug, s-bahn, u-bahn, stadtbahn, tram, stadtbus, regionalbus, schnellbus,
+seilbahn, schiff, ast, sonstige
+
+=item B<-o>, B<--offset> I<minutes> (monitor)
+
+Ignore departures which are less than I<minutes> from now.
+
+=item B<-O>, B<--output> I<outputtypes> (monitor)
+
+For each result, show additional information as specified by I<outputtypes>.
+I<outputtypes> is a comma-separated list, the B<-O>/B<--output> option may
+also be repeated. Each output type has both a short and long form, for instance
+both "-Or" and "--output=route" are valid.
+
+The following output types are supported:
+
+=over
+
+=item a / route_after
+
+Show each departure's full route (timestamps and stop names) after the
+requested station.
+
+=item b / route_before
+
+Show each departure's full route (timestamps and stop names) before the
+requested station.
+
+=item f / fullroute
+
+Show each departure's full route (timestamps and stop names) before and
+after the requested station.
+
+=item i / info
+
+Show station-specific information messages. These typically relate to
+construction work, broken elevators or escalators, or special announcements
+for large-scale events.
+
+=item r / route
+
+Show up to three stops between the requested station and the departure's
+destination. B<efa-m> tries to display the three most important stops,
+however these are heuristically determined and may not be optimal.
+
+=item m / messages
+
+Show free-text messages associated with individual departures. These can
+include generic information such is bicycle transportation options or Wi-Fi
+availability, delay reasons, and more.
+
+=back
+
+=item B<-p>, B<--platform> I<platforms> (monitor)
Only show departures at I<platforms> (comma-separated list, option may be
repeated). Note that the C<< Bstg. >> / C<< Gleis >> prefix must be omitted.
-=item B<-t>, B<--time> I<hh:mm>
+=item B<--raw-json>
+
+Print unprocessed EFA response as JSON and exit.
+Useful for debugging and development purposes.
+
+=item B<-r>, B<--relative> (monitor)
+
+Show relative departure times in minutes (i.e. the time difference between
+the departure and the time of the request). In this case, realtime data is
+already included.
-Show departures starting at I<time> instead of now
+=item B<-s>, B<--service> I<name>
-=item B<-V>, B<--version>
+Short name of the EFA service. See Travel::Status::DE::EFA(3pm) and the
+B<--list> option for a list of services.
+
+=item B<-t>, B<--time> I<hh:mm> (monitor)
+
+Show departures starting at I<time> instead of now.
+
+=item B<--timeout> I<seconds>
+
+Set timeout for HTTP requests. Default: 10 seconds. Set to 0 or a negative
+value to disable it.
+
+=item B<-v>, B<--via> I<station> (monitor)
+
+Only show trains serving I<station> after the requseted stop, and show the
+arrival time at I<station> after the departure time at the current stop.
+I<station> is matched against the "I<city> I<stop>" fields in each line's
+route. Regular expressions are also supported.
+
+=item B<--version>
Show version information.
@@ -216,7 +846,8 @@ Show version information.
=head1 EXIT STATUS
-Zero.
+Normally zero. B<1> means B<efa-m> was called with invalid options,
+B<2> indicates a request error from Travel::Status::DE::EFA(3pm).
=head1 CONFIGURATION
@@ -230,17 +861,30 @@ None.
=item * LWP::UserAgent(3pm)
-=item * XML::LibXML(3pm)
+=item * Travel::Status::DE::EFA(3pm)
=back
=head1 BUGS AND LIMITATIONS
-Unknown.
+B<efa-m> uses the VRR EFA service by default, which seems to contain the
+greatest available set of information. However, some cities (e.g. Berlin or
+parts of Hamburg) are not supported there, and B<efa-m> is not yet able to
+choose the appropriate EFA URL for these by itself. In these cases, you should
+find an appropriate EFA service using the B<-D>/B<--discover> option and then
+use B<-s> I<service> when making requests.
+
+=over
+
+=item * EFA does not provide real-time data for the routes of requested
+departures. Hence, B<--via> estimates the arrival time from scheduled
+departure and departure delay
+
+=back
=head1 AUTHOR
-Copyright (C) 2011 by Daniel Friesel E<lt>derf@finalrewind.orgE<gt>
+Copyright (C) 2011-2023 Birte Kristina Friesel E<lt>derf@finalrewind.orgE<gt>
=head1 LICENSE