vingerha / gtfs2

Support GTFS in Home Assistant GUI-only
https://github.com/vingerha/gtfs2
MIT License
65 stars 4 forks source link

[FEATURE]: Add "origin-stop-name" and "destination-stop-name" in sensors #72

Closed FabienD74 closed 3 weeks ago

FabienD74 commented 1 month ago

Describe the solution you'd like

We have the "long route name" and "direction" (0=normal, 1=reverse) but it's not convenient. We have to know what direction means to retreive the origin and destination.

When i'm waiting for a bus, the only relevant information are:

And it is the same story each time we have to deal with direction(0/1).

Describe alternatives you've considered

Split long route name then take the first or last item,... but I don't know where to split. ( "space", "-" , " - " )
I think it's better/safer to have the real "stop names" of origin and destination.

Additional context Add any other context or screenshots about the feature request here.

I tested the following code:

( Sorry it is my first piece of code in Python and also inside Home Assistant ) .. :-)

def get_route_origin_destination(schedule, route_id, direction):
    _LOGGER.debug("get_route_origin_destination for route: %s", route_id)
    sql_stops = f"""
    SELECT distinct(s.stop_id), s.stop_name, st.stop_sequence
    from trips t
    inner join stop_times st on st.trip_id = t.trip_id
    inner join stops s on s.stop_id = st.stop_id
    where  t.route_id = '{route_id}'
    and (t.direction_id = {direction} or t.direction_id is null)
    order by st.stop_sequence
    """  # noqa: S608
    result = schedule.engine.connect().execute(
        text(sql_stops),
        {"q": "q"},
    )
    str_origin = ""
    str_destination = ""

    for row_cursor in result:
        row = row_cursor._asdict()
        if str_origin == "":
            str_origin = row['stop_name']
        str_destination = row['stop_name']    

    result_tab = []
    result_tab.append (str_origin)
    result_tab.append (str_destination)
    return result_tab

Then in " get_local_stop_list(hass, schedule, data): "


    for row_cursor in result:
        row = row_cursor._asdict()
        _LOGGER.debug("Row from query: %s", row)
        if row["stop_id"] != prev_stop_id and prev_stop_id != "": 
            local_stops_list.append(prev_entry)
            timetable = []
        entry = {"stop_id": row['stop_id'], 
                "stop_name": row['stop_name'], 
                "latitude": row['latitude'], 
                "longitude": row['longitude'], 
                "departure": timetable, 
                "offset": offset}
        self._icon = ICONS.get(row['route_type'], ICON)
##############################
##############################
        self._origin_stop_name = ""
        self._destination_stop_name = ""
        origin_destination = []
        origin_destination =  get_route_origin_destination(schedule, row['route_id'], row["direction_id"])
        self._origin_stop_name =  origin_destination[0]
        self._destination_stop_name =  origin_destination[1]
##############################
##############################

and we also need to update the append statement like:

                timetable.append({"departure": row["departure_time"], 
                                "departure_realtime": departure_rt, 
                                "delay_realtime": delay_rt, 
                                "date": now_date, 
                                "stop_name": row['stop_name'], 
                                "route": row["route_short_name"], 
                                "route_long": row["route_long_name"], 
                                "headsign": row["trip_headsign"], 
                                "trip_id": row["trip_id"], 
                                "direction_id": row["direction_id"], 
                                "origin_stop_name": self._origin_stop_name,
                                "destination_stop_name": self._destination_stop_name,
                                "icon": self._icon})
FabienD74 commented 1 month ago

And my test case is now as follow:

{%- set keyword = "phone" %}
{%- set unique_entities = states | list %}
{%- set ns = namespace(integrations = []) %}
{%- set ns.result = [] %}

{%- for entity in unique_entities %}
  {%- set l_indentifiers = device_attr(entity.entity_id, "identifiers") %}
  {%- set ns.l_indent1 = "" %}
  {%- set ns.l_indent2 = "" %}
  {%- if (l_indentifiers | string) != "None" %}
  {%- set ids = l_indentifiers | list | first| default %}
  {%- if ids and ids | length == 2 %}
  {%- set ns.l_indent1 = ids[0] %}
  {%- set ns.l_indent2 = ids[1] %}
  {%- endif %}
  {%- endif %}

  {%- if ( entity.domain  == "sensor"  ) and ( ns.l_indent1 == "gtfs2")  %}
  {%- if ( keyword in ns.l_indent2 )  %}

  {%- if 1  == 2  %}
-
1 {{ entity.entity_id }} 
2 {{ entity.name }}
3 {{ entity.last_updated }}
4 {{ entity.domain }}
5 {{ entity.state }}
6 {{ entity.attributes }}
7 {{ entity.object_id }}
8 {{ entity.context }}
9 {{ l_indentifiers }}
  - 9a={{ ns.l_indent1 }}
  - 9b={{ ns.l_indent2 }}
  {%- endif %}

  {%- for entry_departure in  entity.attributes['next_departures_lines']  %}
  {%- set stop_name = entry_departure['stop_name'] %}
  {%- set departure = entry_departure['departure'] %}
  {%- set icon      = entry_departure['icon'] %} 
  {%- set direction = entry_departure['direction_id'] %} 
  {%- set route     = entry_departure['route'] %} 
  {%- set route_long = entry_departure['route_long'] %}
  {%- set origin_stop_name = entry_departure['origin_stop_name'] %} 
  {%- set destination_stop_name = entry_departure['destination_stop_name'] %}
  {%- if direction == 1 %}
  {%- set terminus = route_long.split(" - ")[0] %}
  {%- else %}
  {%- set terminus = route_long.split(" - ")[-1] %}
  {%- endif %}
  {%- set item = { 'sort_key1':stop_name , 'sort_key2': departure, 'icon': icon , 'route':route, 'departure':departure, 'stop_name':stop_name, 'origin_stop_name':origin_stop_name, 'destination_stop_name':destination_stop_name, 'terminus': terminus } %}
  {%- set ns.result = ns.result + [item] %}
  {%- endfor -%}

  {%- endif %}
  {%- endif %}
{%- endfor -%}

{%- set list_unique_key1 = ns.result | sort(attribute='sort_key1' , reverse=false) | map(attribute='sort_key1') | unique%}

{%- for unique_key1 in list_unique_key1 %}
- From {{unique_key1 }}

{%- set sorted = ns.result | selectattr('sort_key1', 'eq', unique_key1) |sort(attribute='sort_key2' , reverse=false)%}
{%- for item in sorted %}
{{ item['departure'] + ' - ' + item['icon']+', '+item['route']   + ' to ' + item['destination_stop_name'] }}
{%- endfor %}

{%- endfor %}

Output:

From QUENAST Chemin Bloquiau 13:18:00 - mdi:bus, 116 to TUBIZE Gare - Quai 2 13:54:00 - mdi:bus, 116 to TUBIZE Gare - Quai 2 14:05:00 - mdi:bus, 116 to SOIGNIES Gare 14:54:00 - mdi:bus, 116 to TUBIZE Gare - Quai 2 15:05:00 - mdi:bus, 116 to SOIGNIES Gare From QUENAST Vélodrome 13:20:00 - mdi:bus, 116 to TUBIZE Gare - Quai 2 13:56:00 - mdi:bus, 116 to TUBIZE Gare - Quai 2 14:04:00 - mdi:bus, 116 to SOIGNIES Gare 14:56:00 - mdi:bus, 116 to TUBIZE Gare - Quai 2 15:04:00 - mdi:bus, 116 to SOIGNIES Gare From TUBIZE Au Renard 13:22:00 - mdi:bus, 116 to TUBIZE Gare - Quai 2 13:58:00 - mdi:bus, 116 to TUBIZE Gare - Quai 2 14:02:00 - mdi:bus, 116 to SOIGNIES Gare 14:58:00 - mdi:bus, 116 to TUBIZE Gare - Quai 2 15:02:00 - mdi:bus, 116 to SOIGNIES Gare From TUBIZE Botman 13:24:00 - mdi:bus, 116 to TUBIZE Gare - Quai 2 14:00:00 - mdi:bus, 116 to TUBIZE Gare - Quai 2 14:00:00 - mdi:bus, 116 to SOIGNIES Gare 15:00:00 - mdi:bus, 116 to TUBIZE Gare - Quai 2 15:00:00 - mdi:bus, 116 to SOIGNIES Gare

vingerha commented 1 month ago

we have to know what direction means

Sadly there is no sound/proof logic, I have a bus from Grasse to Valbonne with is outoging 0 and another from Grasse to Nice which is 1...no clue how this is chosen and there are no indicators. My provider Zou does not even provide headsigns in the gtfs, which for local_stops is the only indicator available.

I did not check the code yet as it is first the logic that has to pass (which imo does not apply)... I.e. a route A - B can have multiple trips in direction 0 and multiple in direction 1. The route has no indicator of direction so if the trip does not specify more than just 0 or 1, how would you know what to get? example, I am interested in the third stop of route A-B This is the info I get from the trips bus 1: direction 0 , 12:10 bus 2: direction 1 , 12:12 How to tell which direction they are going ? To A ? To B?

Note that for the other setup with start/end you have to indicate the end-points so here it will always work.

FabienD74 commented 1 month ago

Well I re-used the logic to retreive "stops" of given "route" ... Don't know if this is OK/To be tested/ to be improved.... ;-)

vingerha commented 1 month ago

if you are on the stop that is the first one of the route then this may (!) work...if you are not, no clue With me I have trips: zou: no headsign palmbus: headsign envibus: no comment ter: headsign

FabienD74 commented 1 month ago

I'm in the middle of the route..... something like stop number 10 of 30...

I'm impatient to test the existing logic saturday/sunday. We have specific bus in my area, ...with differents origins/destinations.

I could have bus 115 during the week. 116 during week-end, and something else during rush hours .... I'm curious ...

=> i'm not sure if asking the route in the setup is a good thing.... may be more focused on "stops"... if "stops ID" are shared between routes .... ;-) ;-)

FabienD74 commented 1 month ago

Well, i moved with my phone and laptop.... just to discover that sensors may not have "next_departures_lines" ;-)))) And my small example was just crashing....

Quick fix :


{%- set keyword = "phone" %}

{%- set unique_entities = states | list %}
{%- set ns = namespace(integrations = []) %}

{%- set ns.result = [] %}

{%- for entity in unique_entities %}

  {%- set l_indentifiers = device_attr(entity.entity_id, "identifiers") %}
  {%- set ns.l_indent1 = "" %}
  {%- set ns.l_indent2 = "" %}
  {%- if (l_indentifiers | string) != "None" %}
  {%- set ids = l_indentifiers | list | first| default %}
  {%- if ids and ids | length == 2 %}
  {%- set ns.l_indent1 = ids[0] %}
  {%- set ns.l_indent2 = ids[1] %}
  {%- endif %}
  {%- endif %}

  {%- if ( entity.domain  == "sensor"  ) and ( ns.l_indent1 == "gtfs2")  %}
  {%- if ( keyword in ns.l_indent2 )  %}

  {%- if 1  == 2  %}
-
1 {{ entity.entity_id }} 
2 {{ entity.name }}
3 {{ entity.last_updated }}
4 {{ entity.domain }}
5 {{ entity.state }}
6 {{ entity.attributes }}
7 {{ entity.object_id }}
8 {{ entity.context }}
9 {{ l_indentifiers }}
  - 9a={{ ns.l_indent1 }}
  - 9b={{ ns.l_indent2 }}
  {%- endif %}

  {%- if entity.attributes['next_departures_lines'] is defined %}

    {%- for entry_departure in  entity.attributes['next_departures_lines']  %}
      {%- set stop_name = entry_departure['stop_name'] %}
      {%- set departure = entry_departure['departure'] %}
      {%- set icon      = entry_departure['icon'] %} 
      {%- set direction = entry_departure['direction_id'] %} 
      {%- set route     = entry_departure['route'] %} 
      {%- set route_long = entry_departure['route_long'] %}
      {%- set origin_stop_name = entry_departure['origin_stop_name'] %} 
      {%- set destination_stop_name = entry_departure['destination_stop_name'] %}
      {%- if direction == 1 %}
        {%- set terminus = route_long.split(" - ")[0] %}
      {%- else %}
        {%- set terminus = route_long.split(" - ")[-1] %}
      {%- endif %}
      {%- set item = { 'sort_key1':stop_name , 'sort_key2': departure, 'icon': icon , 'route':route, 'departure':departure, 'stop_name':stop_name, 'origin_stop_name':origin_stop_name, 'destination_stop_name':destination_stop_name, 'terminus': terminus } %}
      {%- set ns.result = ns.result + [item] %}
    {%- endfor -%}

  {%- endif %}

  {%- endif %}
  {%- endif %}
{%- endfor -%}

{%- set list_unique_key1 = ns.result | sort(attribute='sort_key1' , reverse=false) | map(attribute='sort_key1') | unique%}

{%- for unique_key1 in list_unique_key1 %}
- From {{unique_key1 }}

{%- set sorted = ns.result | selectattr('sort_key1', 'eq', unique_key1) |sort(attribute='sort_key2' , reverse=false)%}
{%- for item in sorted %}
{{ item['departure'] + ' - ' + item['icon']+', '+item['route']   + ' to ' + item['destination_stop_name'] }}
{%- endfor %}

{%- endfor %}
FabienD74 commented 1 month ago

Updated version:


{%- set keyword = "gtfs2_arround_phone_local_stoplist" %}

{%- set unique_entities = states | list %}
{%- set ns = namespace(integrations = []) %}
{%- set ns.result = [] %}
{%- for entity in unique_entities %}
  {%- set l_indentifiers = device_attr(entity.entity_id, "identifiers") %}
  {%- set ns.l_indent1 = "" %}
  {%- set ns.l_indent2 = "" %}
  {%- if (l_indentifiers | string) != "None" %}
    {%- set ids = l_indentifiers | list | first| default %}
    {%- if ids and ids | length == 2 %}
      {%- set ns.l_indent1 = ids[0] %}
      {%- set ns.l_indent2 = ids[1] %}
    {%- endif %}
  {%- endif %}
  {%- if ( entity.domain  == "sensor"  ) and ( ns.l_indent1 == "gtfs2") and 
    ( keyword in entity.entity_id )
 %}
  {%- if 1  == 2 %}
- 1 {{ entity.entity_id }} 
2 {{ entity.name }}
3 {{ entity.last_updated }} ( {{ ( as_timestamp(now()) - as_timestamp (entity.last_updated))|int(0)}} seconds ago )
4 {{ entity.domain }}
5 {{ entity.state }}
6 {{ entity.attributes }}
7 {{ entity.object_id }}
8 {{ entity.context }}
9 {{ l_indentifiers }}
  - 9a={{ ns.l_indent1 }}
  - 9b={{ ns.l_indent2 }}
  {%- endif %}

  {%- if entity.attributes['gtfs_updated_at'] is defined %}
      {%- set gtfs_updated_at = entity.attributes['gtfs_updated_at'] %}
  {% else %}
      {%- set gtfs_updated_at = "NA" %}
  {% endif %}

  {%- if entity.attributes['next_departures_lines'] is defined %}
    {%- for entry_departure in  entity.attributes['next_departures_lines']  %}
      {%- set stop_name = entry_departure['stop_name'] %}
      {%- set departure = entry_departure['departure'] %}      
      {%- set icon      = entry_departure['icon'] %} 
      {%- set route     = entry_departure['route'] %} 
      {%- set route_long = entry_departure['route_long'] %}
      {%- set origin_stop_name = "" %} 
      {%- if entry_departure['origin_stop_name'] is defined %}
        {%- set origin_stop_name = entry_departure['origin_stop_name'] %} 
      {%- else %}      
        {%- set origin_stop_name = entry_departure['stop_name'] %} 
      {%- endif %}      
      {%- set destination_stop_name = "" %}
      {%- if entry_departure['destination_stop_name'] is defined %}
        {%- set destination_stop_name = entry_departure['destination_stop_name'] %}
      {%- else %}      
        {%- set destination_stop_name = "N/A" %}
      {%- endif %}      
      {%- if direction == 1 %}
        {%- set terminus = route_long.split(" - ")[0] %}
      {%- else %}
        {%- set terminus = route_long.split(" - ")[-1] %}
      {%- endif %}
      {%- set item = { 'sort_key1':stop_name , 'sort_key2': departure, 'gtfs_updated_at':gtfs_updated_at, 'icon': icon , 'route':route, 'departure':departure, 'stop_name':stop_name, 'origin_stop_name':origin_stop_name, 'destination_stop_name':destination_stop_name, 'terminus': terminus } %}
      {%- set ns.result = ns.result + [item] %}
    {%- endfor -%}
  {%- endif %}

  {%- if entity.attributes['stops'] is defined %}
    {%- for stop in  entity.attributes['stops']  %}
      {%- set stop_name = stop['stop_name'] %}
      {%- if stop['departure'] is defined %} 
        {%- for entry_departure in  stop['departure']  %}
          {%- set stop_name = entry_departure['stop_name'] %}
          {%- set departure = entry_departure['departure'] %}      
          {%- set icon      = entry_departure['icon'] %} 
          {%- set route     = entry_departure['route'] %} 
          {%- set route_long = entry_departure['route_long'] %}

          {%- set origin_stop_name = "" %} 
          {%- if entry_departure['origin_stop_name'] is defined %}
            {%- set origin_stop_name = entry_departure['origin_stop_name'] %} 
          {%- else %}      
            {%- set origin_stop_name = entry_departure['stop_name'] %} 
          {%- endif %}      
          {%- set destination_stop_name = "" %}
          {%- if entry_departure['destination_stop_name'] is defined %}
            {%- set destination_stop_name = entry_departure['destination_stop_name'] %}
          {%- else %}      
            {%- set destination_stop_name = "N/A" %}
          {%- endif %}      

          {%- set item = { 'sort_key1':stop_name , 'sort_key2': departure, 'gtfs_updated_at':gtfs_updated_at, 'icon': icon , 'route':route, 'departure':departure, 'stop_name':stop_name, 'origin_stop_name':origin_stop_name, 'destination_stop_name':destination_stop_name, 'terminus': terminus } %}
          {%- set ns.result = ns.result + [item] %}
        {%- endfor -%}
      {%- endif %}
    {%- endfor -%}  
  {%- endif %}  
  {%- endif %}
{%- endfor -%}

{%- set list_unique_key1 = ns.result | sort(attribute='sort_key1' , reverse=false) | map(attribute='sort_key1') | unique%}
{%- for unique_key1 in list_unique_key1 %}

{{unique_key1 }}
{%- set sorted = ns.result |selectattr('sort_key1', 'eq', unique_key1) |sort(attribute='sort_key2' , reverse=false) %}
{%- for item in sorted %}
  {%- set updated_seconds = ( as_timestamp(now()) - as_timestamp (item['gtfs_updated_at']))|int(0) %}
  {%- if ( updated_seconds > 300 )  %}
        {%- set warning_sign = " [upd " + ( updated_seconds| string ) + 's ago]' %}
  {%- else %}
        {%- set warning_sign = " (" + ( updated_seconds| string ) + 's)'%}
  {%- endif %}  
{{ " " +item['departure'] + ' - ' + item['icon']+', '+item['route']   + ' to ' + item['destination_stop_name'] + warning_sign }}
{%- endfor %}
{%- endfor %}
vingerha commented 1 month ago

I would love to understand what you are trying to get out of all that code, there should be a simpler solution if this is of interest for others too. Is this only to identify the trip direction?

FabienD74 commented 1 month ago

My goal: to be able to grab my phone, open HA, and have a list of all BUS departure around me. Should work at home (not moving) and when i'm moving.... walking/running ... driving? in the train ?

Only that. A stupid list on a dedicated dahboard

what do you suggest ?

FabienD74 commented 1 month ago

and if you install "Own tracks" on your phone, you can customize

but i'm not using it, as i have to open it. I should try it again, with minimum movements 1 meter, update frequency 2 seconds.... that would hurt ...

vingerha commented 1 month ago

I agree with an wanting to see data that is (close-to) now but I also know that this may compromise the performance if one is using multiple datasources. I have 3 (countries, of which CH and NL are huge db), my son 3 local, my daughter uses the NL one. I myself donot need an update of the CH or NL ones if I am in F as you understand it should not even poll for local stops then, let alone realtime.... by using automations/= with/out servicecalls one can trigger the automation to refresh the local stops, in the autmation I could build logic that takes care of location/time-of-day already so this I am covering right now and I do not need to have this logic covered by gtfs2 as each user may have a different view on things. In short: updates should ideally(!) be done via HA ootb functionality or configurable or a combination

vingerha commented 3 weeks ago

Closing this topic, awaiting a full design proposal, i.e. not a solution yet as I may reject that and all the work would be for nothing